-
Notifications
You must be signed in to change notification settings - Fork 0
/
content.json
1 lines (1 loc) · 926 KB
/
content.json
1
[{"title":"如何将镜像体积海量缩减","date":"2023-07-31T08:53:09.000Z","path":"2023/07/31/image-build/","text":"镜像的传统构建我们随便找个Golang代码项目作为案例,来开始构建一个镜像。下面我们以我的一个实战项目开始讲解:https://gitee.com/damon_one/uranus。 第一步:我们把项目代码克隆到本地: 1git clone https://gitee.com/damon_one/uranus 第二步,书写其编译的Dockerfile: 12345FROM golang:1.20WORKDIR /opt/appCOPY . .go build -o hz-zeus ./zeusCMD [\"/opt/app/hz-zeus\"] 这个 Dockerfile 描述的构建过程非常简单,我们首选 Golang:1.20 版本的镜像作为编译环境,将源码拷贝到镜像中,然后运行 go build 编译源码生成二进制可执行文件,最后配置启动命令。 第三步,构建镜像: 1docker build -t hz-zeus -f Dockerfile . 这样编译构建的镜像会很大,这里就不展示最后的镜像信息了。 Dockerfile 优化从上面的 Dockerfile 可以看出,我们在容器内运行了 go build -o hz-zeus ./zeus,这条命令将会编译生成二进制的可执行文件,由于编译的过程中需要 Golang 编译工具的支持,所以我们必须要使用 Golang 镜像作为基础镜像,这是导致镜像体积过大的直接原因。 既然依赖基础镜像比较大,那么我们是否可以替换为轻量级的镜像呢?发现可以将 Golang:1.20 基础镜像替换为 golang:1.20-alpine 版本。 但,这样的构建之后,发现镜像还是很大。毕竟是在镜像内镜像编译二进制文件后构建镜像。那是否可以在外部进行构建后再同步到镜像内部呢? 123$ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o hz-zeus ./zeus$ ls -lh... 最简单的办法就是在本地先编译出可执行文件,再将它复制到一个更小体积的 ubuntu 镜像内。具体做法是,首先在本地使用交叉编译生成 Linux 平台的二进制可执行文件。接下来,使用 Dockerfile 文件构建镜像。 12345FROM ubuntu:latestWORKDIR /opt/appCOPY hz-zeus ./CMD [\"/opt/app/hz-zeus\"] 因为不再需要在容器里进行编译,所以我们直接引入了不包含 Golang 编译工具的 ubuntu 镜像作为基础运行环境,接下来使用 docker build 命令构建镜像。 这种构建方式生成的镜像在体积上比最初的缩小了几乎 90% 。镜像的最终大小就相当于 ubuntu:latest 的大小加上 Golang 二进制可执行文件的大小。不过,这种方式将应用的编译过程拆分到了宿主机上,这会让 Dockerfile 失去描述应用编译和打包的作用,不是一个好的实践。 多阶段构建多阶段构建的本质其实就是将镜像构建过程拆分成编译过程和运行过程。第一个阶段对应编译的过程,负责生成可执行文件;第二个阶段对应运行过程,也就是拷贝第一阶段的二进制可执行文件,并为程序提供运行环境,最终镜像也就是第二阶段生成的镜像。 1234567891011FROM golang:1.20 as builderWORKDIR /opt/appCOPY . .RUN go build -o hz-zeus ./zeusFROM ubuntu:latestWORKDIR /opt/appCOPY --from=builder /opt/app/hz-zeus ./hz-zeusCMD [\"/opt/app/hz-zeus\"] 这段内容里有两个 FROM 语句,所以这是一个包含两个阶段的构建过程。 第二阶段,它的作用是将第一阶段生成的二进制可执行文件复制到当前阶段,把 ubuntu:latest 作为运行环境,并设置 CMD 启动命令。 最后,我们执行docker build后会发现镜像大小与上面的先编译后copy到镜像种的操作生成的镜像一样大小。 到这里,对镜像大小的优化已经基本上完成了,镜像大小也在可接受的范围内。在实际的项目中,我也推荐你使用 ubuntu:latest 作为第二阶段的程序运行镜像。 如何复用构建缓存在第一阶段的构建过程中,我们先是用 COPY . . 的方式拷贝了源码,又进行了编译,这会产生一个缺点,那就是如果只是源码变了,但依赖并没有变,Docker 将无法复用依赖的镜像层缓存。在实际构建过程中,你会发现 Docker 每次都会重新下载 Golang 依赖。 这就引出了另外一个构建镜像的小技巧:尽量使用 Docker 构建缓存。 要使用 Golang 依赖的缓存,最简单的办法是:先复制依赖文件,再下载依赖,最后再复制源码进行编译。基于这种思路,我们可以将第一阶段的构建修改如下: 123456FROM golang:1.20 as builderWORKDIR /opt/appCOPY go.* ./RUN go mod downloadCOPY . .RUN go build -o hz-zeus ./zeus 这样,在每次代码变更而依赖不变的情况下,Docker 都会复用之前产生的构建缓存,这可以加速镜像构建过程。 Java 案例: 1234567891011121314151617# First stage: complete build environmentFROM maven:3.5.0-jdk-8-alpine AS builder# To resolve dependencies in a safe way (no re-download when the source code changes)ADD ./pom.xml pom.xmlRUN mvn install -Dmaven.repo.local=./.m2ADD ./src src/# package jarRUN mvn -Dmaven.repo.local=./.m2 install -Dmaven.test.skip=trueFrom openjdk:8# copy jar from the first stageCOPY --from=builder target/my-app-1.0-SNAPSHOT.jar my-app-1.0-SNAPSHOT.jarEXPOSE 8080CMD [\"java\", \"-jar\", \"my-app-1.0-SNAPSHOT.jar\"] 关于作者 笔名:Damon,技术爱好者,微服务架构设计,云原生、容器化技术,现从事Go相关,涉及云原生、边缘计算、AI人工智能、云产品Devops落地实践等云原生技术。拿过专利。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注 AI绘画扫我 星球","tags":[{"name":"后端","slug":"后端","permalink":"http://damon008.github.io/tags/%E5%90%8E%E7%AB%AF/"},{"name":"运维","slug":"运维","permalink":"http://damon008.github.io/tags/%E8%BF%90%E7%BB%B4/"}]},{"title":"Springboot 微服务应用启动慢的克星","date":"2023-07-31T06:18:52.000Z","path":"2023/07/31/java-startup-analyzer/","text":"背景随着业务的复杂程度越来越大,所启动的实例或函数越来越多,Spring cloud 应用的启动越来越慢,那么如何发现 Spring 容器启动慢的原因或位置,有没有一款工具,帮助我们用户发现 Spring 应用启动慢的位置呢?同时,还可以提供 Spring Bean 异步初始化的工具。那么答案是有的。 实战操作下面,我们可以通过下面的方法尝试分析一下自己的应用吧,Let us go~ 安装工具组件第一步:在 gitlab 网站下面其最新 tag:https://github.com/linyimin0812/spring-startup-analyzer/releases/tag/v2.0.5,下载tar.gz包。 第二步:解压下载的安装包,记住解压后的路径,下面一步要用: win 下直接工具解压 linux 或 mac 通过 tar -zxvf 压缩文件名.tar.gz 解压 项目参数设置第一步:编辑 Spring Boot 的启动参数,包括: 该工具采用 agent 的方式启动,所以要添加参数-javaagent:$HOME/spring-startup-analyzer/lib/spring-profiler-agent.jar,这里 $HOME 代表以前的解压路径,记得根据上面解压后的路径编辑这个参数,我的是:-javaagent:G:/Downloads/spring-startup-analyzer/lib/spring-profiler-agent.jar 配置分析工具的参数,这里根据自己需要添加即可,比如可以配置超时时间 10 分钟:-Dspring-startup-analyzer.app.health.check.timeout=10,其他可配置项如下表,你可以工具自己应用的情况去修改: 英文版: 中文版: 1234567891011121314151617spring-startup-analyzer: admin: http: server: port: 8065 app: health: check: endpoints: \"http://127.0.0.1:7002/actuator/health\" timeout: 10 async: profiler: interval: millis: 5 sample: thread: names: main 最后一步:查看该工具的日志,可以通过 $HOME/spring-startup-analyzer/logs路径,这里 $HOME 代表以前的解压路径,日志文件的类别为: startup.log: 启动过程中的日志 transform.log: 被 re-transform 的类/方法信息 应用启动完成后会在 console 和 startup.log 文件中输出======= spring-startup-analyzer finished, click http://localhost:8065 to visit details. ======,可以通过此输出来判断采集是否完成。 当然,该组件还支持自定义扩展,更多详情请看:https://github.com/linyimin0812/spring-startup-analyzer/tree/v2.0.5#configuration。 接入异步 Bean 优化这里提到了一个启动加速的优化思路,就是把一些耗时的 Bean 初始化改成异步就能实现。该项目提供了 Bean 的异步初始化工具,也非常好用,只需要下面几步就能完成。提供一个 Spring Bean 异步初始化 jar 包,针对初始化耗时比较长的 bean,异步执行 init 和@PostConstruct 方法提高应用启动速度。 第一步:引入依赖 12345<dependency> <groupId>io.github.linyimin0812</groupId> <artifactId>spring-async-bean-starter</artifactId> <version>2.0.5</version></dependency> 第二步:配置参数 123456789101112spring-startup-analyzer: boost: spring: async: ## 指定异步的Bean名称 bean-names: restTemplate,testBean,testComponent #异步化的Bean可能在Spring Bean初始化顺序的末尾,导致异步优化效果不佳,打开配置优先加载异步化的Bean bean-priority-load-enable: true # 执行异步化Bean初始化方法线程池的核心线程数 init-bean-thread-pool-core-size: 8 # 执行异步化Bean初始化方法线程池的最大线程数 init-bean-thread-pool-max-size: 8 第三步:检查 Bean 是否异步初始化。查看日志$HOME/spring-startup-analyzer/logs/startup.log 文件,对于异步执行初始化的方法,会按照以下格式写一条日志: 1async-init-bean, beanName: ${beanName}, async init method: ${initMethodName} 但是,异步并不是万能的,你还需要注意以下这几点: 应该优先从代码层面优化初始化时间长的 Bean,从根本上解决 Bean 初始化耗时长问题 对于二方包/三方包中初始化耗时长的 Bean(无法进行代码优化)再考虑 Bean 的异步化 对于不被依赖的 Bean 可以放心进行异步化,可以通过各个 Bean 加载耗时中的 Root Bean 判断 Bean 是否被其他 Bean 依赖 对于被依赖的 Bean 需要小心分析,在应用启动过程中不能其他 Bean 被调用,否则可能会存在问题 支持异步化的 Bean 类型支持@Bean, @PostConstruct 及@ImportResource 方式初始化 bean @Bean(initMethod = “init”)标识的 Bean 1234567891011121314@Bean(initMethod = \"init\")public TestBean testBean() { return new TestBean();}@Bean(initMethod = \"init\")public RestTemplate restTemplate() { SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); requestFactory.setReadTimeout(env.getProperty(\"client.http.request.readTimeout\", Integer.class, 15000)); requestFactory.setConnectTimeout(env.getProperty(\"client.http.request.connectTimeout\", Integer.class, 3000)); RestTemplate rt = new RestTemplate(requestFactory); return rt;} @PostConstruct 标识的 Bean 1234567@Componentpublic class TestComponent { @PostConstruct public void init() throws InterruptedException { Thread.sleep(20 * 1000); }} 更多详情请看:https://github.com/linyimin0812/spring-startup-analyzer/blob/v2.0.5/README_ZH.md。 关于作者 笔名:Damon,技术爱好者,微服务架构设计,云原生、容器化技术,现从事Go相关,涉及云原生、边缘计算、AI人工智能、云产品Devops落地实践等云原生技术。拿过专利。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注 AI绘画扫我 星球","tags":[{"name":"后端","slug":"后端","permalink":"http://damon008.github.io/tags/%E5%90%8E%E7%AB%AF/"}]},{"title":"秒级体验本地调试远程k8s中的服务","date":"2023-07-31T06:12:24.000Z","path":"2023/07/31/app-in-k8s/","text":"背景在这个以k8s为云os的时代,程序员在日常的开发过程中,肯定会遇到各种问题,比如:本地开发完,需要部署到远程k8s集群,本地如何直接操作呢?又如:提测到测试环境发现有问题,或者nightly环境本身没过,这时候,可能需要一些调试。下面介绍一款开源已久的产品来体验秒级体验下本地操作远程k8s、直接在调试远程代码。 借助 Nocalhost 实现 k8s 应用秒级的本地开发体验直入主题,Nocalhost 是腾讯云 CODING 在 2020 年开源的项目,同时它也是云原生开发领域下第一个由国人主导并进入 CNCF Sandbox 的项目。 Nocalhost 开发实战安装 Nocalhost 插件首先,需要先安装 Nocalhost IDE 插件。Nocalhost 支持 VS Code 和 Jetbrains 全系列的 IDE,你可以在市场中搜索。 接下来,我以 Jetbrains Goland 插件为例简单介绍如何安装 Nocalhost 插件。 首先,在 IDEA Goland 插件市场中搜索 Nocalhost,然后点击“安装”按钮进行安装,如下图所示。 在安装 IDE 插件之后,Nocalhost 会自动下载 nhctl 工具,你可以在 Jetbrains Goland 的右下角查看下载进度,nhctl 是 Nocalhost 的核心组件,它为插件提供 Kubernetes API 调用能力。 集成远程 k8s 集群接下来,添加 Kubernetes 集群,在右侧菜单栏中打开 Nocalhost 插件,如果你已经提前准备好了 K8s 集群,Nocalhost 就会自动识别,点击“Add”即可添加集群。 在上面的第二步,选择k8s的kubeconfig,选择完后,会自动检测是否存在该集群: 如果不存在该集群,会提示: 最后,在 Add 完成功后,会在该菜单下看到集群相关的信息以及资源: 部署应用接着,我们就可以部署应用了,先来看看部署官方给的示例应用,首先鼠标移到default命名空间位置右击,然后可以看到Deploy App: 点击部署 app后,出现图: 我们点击第四个按钮Deploy Demo,此时,Nocalhost 将自动从 GitHub 克隆示例应用仓库,并将它部署到集群的 default 命名空间下。同时,此时,控制台就会打印如下日志: 此时,表示应用部署成功,Nocalhost 将自动进行端口转发,并打开浏览器访问http://127.0.0.1:39080/productpage示例应用页面,如下图所示: 简单介绍一下这个示例应用,这是一个图书管理系统,展示了书籍的详情信息、评价、作者信息、评分。每部分信息都是由不同的微服务输出的,示例应用一共有 5 个微服务组成,它们分别是 Productpage 服务、Reviews 服务、Details 服务、Rattings 服务和 Authors 服务。其中,Productpage 服务负责输出首页以及请求其他的微服务,也是应用的入口,其他服务根据字面意思分别输出了其他的内容。 秒级开发循环反馈接下来我们来看一下如何使用 Nocalhost 打破传统的开发循环反馈,并获得秒级的 Kubernetes 应用开发体验。 我们在 Nocalhost 插件中点击 default 展开命名空间,然后点击 bookinfo 展开应用,点击 Workload 展开工作负载,最后,点击 Deployment 查看工作负载列表: 此时,将鼠标移动到 authors 服务,点击右侧的“绿色锤子”按钮进入该服务的开发模式。 然后,在弹出的对话框中选择“Clone from Git Repo”,并选择一个本地目录用来存储源码。 首次打开会出现是否信任,直接点击信任: 点击确认后, Nocalhost 将自动克隆 authors 服务的源码到所选择的目录下,并将源码通过新的 UI 窗口打开。 此时,在新的窗口的右下角你会看到 Nocalhost 进入开发模式的提示,等待片刻后,将获得一个远端容器的终端。 注意,这个终端并不是本地的终端,而是 authors 服务在开发模式下的终端。也就是说,在此终端下执行的所有命令实际上都是在 authors 服务的容器里执行的。此时,你可以在终端内执行 ls 命令来查看容器的文件目录。 由于这个容器启动的逻辑是直接通过运行源码,所以这里有源码,并且执行go run app.go: 此时,我们可以任意改代码进行调试了吧~ 容器热加载其实,可以看出 Nocalhost 是通过文件同步的技术来实现本地和远端代码一致的,在实际编码过程中,每次在本地修改源码后,我们往往需要手动重启容器内的业务进程才能看到编码效果。 那么,能不能更进一步,实现修改代码后自动重载呢?Nocalhost 同样也为我们提供了和语言无关的容器热加载,也就是说,当本地有任何代码变更时,Nocalhost 都会自动帮助我们重启容器内的业务进程,达到容器热加载的目的。接下来,我们一起来体验这个功能。首先,在当前 VS Code 窗口中重新打开 Nocalhost 插件,找到 authors 服务。此时,你将看到该服务左侧有一个“绿色锤子”图标,这表示这个服务正在开发模式当中,如下图所示。 接下来,右击 authors 服务,选择一个选项 Remote Run: 注意,在点击 Remote Run 之前,一定要先确保已经通过 Ctrl+C 的方式手动停止了容器内的业务进程,这可以避免重复运行业务进程导致的端口冲突。 现在,Nocalhost 将自动开启一个新的终端,并自动启动业务进程: 到这里,可能有疑惑,Nocalhost 怎么知道我的业务的启动命令呢?答案是通过为 Nocalhost 配置启动命令。你可以通过点击 authors 服务右侧的“设置”按钮,在弹出的对话框中选择“取消”来查看配置文件中的 command.run 字段。实际上,Nocalhost 是通过运行配置的 run.sh 脚本来启动业务的。 最后,你可以在终端窗口中通过 Ctrl+C 的方式来中断容器热加载。到这里,Nocalhost 容器热加载的全过程就已经体验完了。 一键调试除了容器热加载以外,Nocalhost 还为我们提供了便利的一键远程调试功能。同样地,找到 authors 服务,右击选择“Remote Debug”来进入远程调试。 接下来,Nocalhost 就会以调试模式启动业务进程,然后通过 Kubernetes 端口转发的方式将远端的调试端口转发到本地,并控制调试器连接到调试端口。需要注意的是,由于 authors 服务是 Golang 编写的,所以调试依赖于本地的 Golang 开发工具,如果你的电脑里没有 Golang 开发环境,Nocalhost 将提示你安装相关工具和插件。进入调试后,你将看到窗口右下角出现准备连接调试器,如下图所示: 后面就可以打断点进行Debug模式调试了。 在这个调试例子中,如果你用的是 M1 芯片的 Mac,那么你可能会发现在调试过程中 IDE 的调试器一直无法连接到远端容器,这时候,你还需要进行下面的操作。在 Nocalhost 插件中点击 authors 服务的“设置”按钮进入服务的开发配置页,并将 image 字段修改为 okteto/golang:1.19,然后,点击“红色锤子”退出 authors 服务的开发模式,退出完成后,再点击“Remote Debug”来进入调试模式即可。最后,要退出调试模式,你可以切换到 VS Code 终端菜单,并通过 Ctrl+C 的方式来终止调试进程。 到此,就完整的带大家走一圈秒级体验本地远程调试k8s集群的应用服务了。谢谢大家关注~ 关于作者 笔名:Damon,技术爱好者,微服务架构设计,云原生、容器化技术,现从事Go相关,涉及云原生、边缘计算、AI人工智能、云产品Devops落地实践等云原生技术。拿过专利。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注 AI绘画扫我 星球","tags":[{"name":"后端","slug":"后端","permalink":"http://damon008.github.io/tags/%E5%90%8E%E7%AB%AF/"}]},{"title":"无缝插拔分布式链路跟踪","date":"2023-07-26T10:46:35.000Z","path":"2023/07/26/otel/","text":"前因随着业务的扩充,微服务项目越来越多,对于分布式架构设计来讲,如何更好的监控每个服务的上下游成为了重点问题, 因为一旦中间某个调用链发生问题,就会导致整个链路连接失败。为了更好、更快的找到链路所在,我们就需要一个完整的链路跟踪系统,本节主要分享的是基于OpenTelemetry的一个链路跟踪库,可以很方便的无缝插拔式接入各种微服务系统中,当然,推荐使用字节开源的微服务分布式框架:Hertz、Kitex,该套框架已经很好的接入很多插件,并且本身提供高性能的功能特性,如果对于微服务有性能要求的,推荐尝试。 基于高性能RPC框架Kitex前面讲过我们的IDL,并且目前使用idl的两种方式以及使用的场景,接下来,我们直接通过thrift的idl文件来生成一个RPC微服务。 首先第一步,写出这个底层RPC服务的idl逻辑: 123456789101112131415161718192021namespace go teststruct Request { 1: required string data 2: i64 type}struct Msg { 1: i32 code 2: string msg}struct Response { 1: Msg msg 2: string data}service TestService { Response Test1(1: Request req) Response Test2(1: Request req)} 上面写了一个简单的基于thrift的idl,接下来,我们生成一个基于该idl的微服务,执行如下命令: 1kitex -module \"hz-kitex-examples\" -thrift frugal_tag,template=slim -service test idl/test.thrift 执行上面的命令后,我们会看到在kitex_gen目录下生成一堆文件,这里就是RPC协议通信时,需要建立的一个桥梁,提供调用者与被调用者的连接: 同时,我们还会看到生成对应的被调用者的逻辑: 这里是启动类中对于网络、编解码、连接的设置: 这样,一个简单的RPC底层微服务就写完了。接下来,我们需要写一个对应的客户端进行彼此之间的通信、调用: 在这个rpc client的逻辑里,我们需要定义与server端一样的编解码、网络连接、通信协议方式。那么到目前为止,对于一个完整的基于Kitex的RPC微服务就开发完成了,下面环节,我们就基于该框架进行插拔式服务的链路跟踪。 插拔式链路跟踪插拔式链路跟踪,为什么叫插拔式呢?顾名思义,其接入链路跟踪很简单,无需太多的逻辑,即可接入整个服务的连接的链路。本节介绍的链路跟踪技术,主要是基于Opentelemetry协议的Optl,对于该协议技术,大家感兴趣可以参考github的相关资料。 我们先看看RPC server端如何插拔式接入的,很简单,我们直接在server的启动main里加入: 123456p := provider.NewOpenTelemetryProvider( provider.WithServiceName(constants.UserServiceName), provider.WithExportEndpoint(\"121.37.173.206:4317\"), provider.WithInsecure(), ) defer p.Shutdown(context.Background()) 同时,在初始化new服务端的时候,只要加上: 12server.WithSuite(tracing.NewServerSuite()),server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: constants.UserServiceName}), 这样,对于RPC server端就可以接入Opentelemetry了。 然后,对于调用server的client端,我们看如何接入,也很简单: 12client.WithSuite(tracing.NewClientSuite()),client.WithClientBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: constants.ApiServiceName}), 看到代码是不是感觉很简单,两行code即可搞定,对得起我这文章标题吧。 同时,对于RPC client,也是一个Http server服务,所以可以接入open telemetry: 123456789101112131415p := provider.NewOpenTelemetryProvider( provider.WithServiceName(constants.ApiServiceName), provider.WithExportEndpoint(\"121.37.173.206:4317\"), //(\"localhost:4317\"), provider.WithInsecure(), )defer func(ctx context.Context, p provider.OtelProvider) { _ = p.Shutdown(ctx)}(context.Background(), p)tracer, cfg := hertztracing.NewServerTracer()r := server.New( tracer, )r.Use(hertztracing.ServerMiddleware(cfg)) 看着是不是也很简单,代码逻辑都是一样的,没啥复杂的业务逻辑,新手都能入门。 测试最后,我们来看看这个接入微服务链路跟踪后的整个系统的连接链路是啥样的,我们需要简单的展示下,这里是直接使用Jaeger进行UI展示,在调用数次请求之后,我们可以看到如下结果,先来看下我们调用的关系链: 然后经过几次调用后,我们可以看到这样的关系图: 最后,我们可以看到每次调用的链路以及日志信息: 最后,以上就是今天分享的几行代码,就可以轻松的无缝接入链路跟踪,帮助我们很好的看到调用链路关系,以及问题定位。 关于作者 笔名:Damon,技术爱好者,微服务架构设计,云原生、容器化技术,现从事Go相关,涉及云原生、边缘计算、AI人工智能、云产品Devops落地实践等云原生技术。拿过专利。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注 AI绘画扫我 星球","tags":[{"name":"后端","slug":"后端","permalink":"http://damon008.github.io/tags/%E5%90%8E%E7%AB%AF/"},{"name":"hertz","slug":"hertz","permalink":"http://damon008.github.io/tags/hertz/"}]},{"title":"一分钟教你学会如何使用 Hertz 框架","date":"2023-07-25T10:06:12.000Z","path":"2023/07/25/hertz/","text":"Hertz 是什么Hertz[həːts] 是字节 CloudWeGo 团队一个 Golang 微服务 HTTP 框架,在设计之初参考了其他开源框架 fasthttp、gin、echo 的优势, 并结合字节跳动内部的需求,使其具有高易用性、高性能、高扩展性等特点,目前在字节跳动内部已广泛使用。 如今越来越多的微服务选择使用 Golang,如果对微服务性能有要求,又希望框架能够充分满足内部的可定制化需求,Hertz 会是一个不错的选择。 环境基础依赖在开发、部署运行该项目之前,需要有一定的基础环境: go v1.16+ hz 脚手架工具 v0.6.3 支持 win、mac、linux 安装 go直接参考官方文档:https://go.dev/doc/install 安装命令行脚手架工具 hzhz 是 Hertz 框架提供的一个用于生成代码的命令行工具。目前,hz 可以基于 thrift 和 protobuf 的 IDL 生成 Hertz 项目的脚手架。 注意: 确保 GOPATH 环境变量已经被正确地定义(例如 export GOPATH=~/go)并且将 $GOPATH/bin 添加到 PATH 环境变量之中(例如 export PATH=$GOPATH/bin:$PATH);请勿将 GOPATH 设置为当前用户没有读写权限的目录 安装 hz:go install github.com/cloudwego/hertz/cmd/hz@latest 安装完成后执行命令: 1hz -version 出现如下结果,即为安装成功: 1hz version v0.6.4 生成代码与运行代码代码目录确定位置 若将代码放置于 $GOPATH/src 下,需在 $GOPATH/src 下创建代码目录,进入该目录后再进行操作 直接新建一个项目目录 生成代码我们选择方式二:在任一目录新建一个项目uranus,如下图: 由于我们是做一个大的项目工程,类似 Java 的父子工程,所以我们需要先建立一个依赖,新建一个go.mod文件: 1go 1.20 由于这边用的是最新的 go 版本,所以直接用 v1.20。 然后我们新建今天的第一个项目:kronos,由于我们后面的编解码都是通过 idl 文件进行生成,所以这里需要用到idl目录,然后我们再创建一个 pkg 目录,来专门存放工具库,综合目录情况如下: 参考 go 项目基本布局:https://github.com/golang-standards/project-layout/blob/master/README_zh.md 最后,我们需要对当前项目进行环境配置: 设置 go 环境 注:在配置代理时,选择国内的域名进行配置。 插曲在使用 hz 工具生成代码之前,我们需要先了解下编解码序列化工具:thrift、protobuf protocol buffers,谷歌开发的数据序列号格式。以二进制形式有效而紧凑地存储结构化数据,允许在网络连接上更快传输。 thrift 不仅仅提供全套 rpc 解决方案,包括序列化机制、传输层、并发处理框架等的 rpc 服务框架。利用 idl 文件来定义接口和数据类型。通过 thrift 提供的编译器编译成不同语言代码,以此实现跨语言调用。 protobuf 和 json 的区别 速度:在序列化和反序列化数据方面,Protobuf 比 JSON 快得多。由于格式是二进制的,json 是文本格式,Protobuf 中读写结构化数据所需的时间比在 JSON 中要短。 大小:Protobuf 比 JSON 小得多,在网络带宽有限的情况下,由于二进制数据流的紧凑性,存储和传输 Protobuf 信息所占用的空间比 JSON 信息要少。 数据类型:Protobuf 支持更复杂的数据类型,如枚举和 map 平台兼容性:由于 Protobuf 是一种开源格式,语言和平台独立的,它可以在多个平台上使用而没有困难或兼容性问题。 手写一个 IDL 文件在前面,我们了解到 thrift、protobuf 是什么之后,我们先来手写一个 IDL 文件: 12345678910111213141516namespace go hellostruct Request { 1: string name 2: string age}struct Response { 1: i8 code 2: string msg 3: map<string,string> data}service HelloApi { Response echo(1: Request req)} 上面写的是一个基于 thrift 的 IDL,同样,我们也可以基于: 123456789101112131415161718192021222324252627282930syntax = \"proto3\";package elena;option go_package = \"elena\";message BaseResp { int16 code = 1; string msg = 2;}message Elena { int64 id = 1; string name = 2; string pthone = 3; string password = 4;}message CreateRequest { string name = 1; string password = 2; string pthone = 3;}message CreateResponse { BaseResp result = 1; string data = 2;}service ElenaService { rpc CreateElena (CreateRequest) returns (CreateResponse) {}} 当然,由于 thrift、protobuf 在不同场景下具有不同的特性与性能,一般: 基于 Streaming 场景下,基于 Protobuf 编码,有两种:Kitex Protobuf 和 gRPC。性能较快 其它场景基本基于 thrift 进行序列化编解码即可 代码生成我们按照前面写的 thrift 模板文件 idl,来依赖 hz 工具生成,在生成代码之前,需要安装相应的编译器 thriftgo、protoc: 1go install github.com/cloudwego/thriftgo@latest 对于 protoc,可以参考: 123456789// brew 安装$ brew install protobuf// 官方镜像安装,以 macos 为例$ wget https://github.com/protocolbuffers/protobuf/releases/download/v3.19.4/protoc-3.19.4-osx-x86_64.zip$ unzip protoc-3.19.4-osx-x86_64.zip$ cp bin/protoc /usr/local/bin/protoc// 确保 include/google 放入 /usr/local/include下$ cp -r include/google /usr/local/include/google 也可以参考官方:https://github.com/protocolbuffers/protobuf 安装完成编译器后,我们进入目录kronos,执行: 12345hz new -module kronos --idl ../idl/hello.thrift -t=template=slimhz update ../idl/hello.thrift -t=template=slimgo mod tidy 注意,在生成代码后,需要进行微调,目录结构、go.mod 等不同,会出现一些小问题,同时需要执行:go mod tidy进行整理。 运行项目执行文件:main.go 启动之后,看控制台: 可以看到有几个接口,同时当前服务默认监听端口:8888。 打开一个web ui,访问API接口: 关于作者 笔名:Damon,技术爱好者,微服务架构设计,云原生、容器化技术,现从事Go相关,涉及云原生、边缘计算、AI人工智能、云产品Devops落地实践等云原生技术。拿过专利。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注 AI绘画扫我 星球","tags":[{"name":"后端","slug":"后端","permalink":"http://damon008.github.io/tags/%E5%90%8E%E7%AB%AF/"},{"name":"hertz","slug":"hertz","permalink":"http://damon008.github.io/tags/hertz/"}]},{"title":"Linux 常用命令","date":"2023-02-02T11:34:14.000Z","path":"2023/02/02/linux-cmd/","text":"查系统空间:df -h查时间:kubectl get eventsredis监听:ps -ef | grep 6379netstat -lntp | grep 6379sudo netstat -apn | grep 6379redis目录:/usr/local/redis/bin/usr/local/redis/etc:配置conf进入redis:redis-cli -h localhost -p 6379 -a 密码 CONFIG GET maxclientsCONFIG set maxclients 10000 测试时间:curl -o /dev/null -s -w ‘%{time_connect}:%{time_starttransfer}:%{time_total}\\n’ ‘http://10.10.1.8:9086/v1/Frameworks' 一、文件和目录 cd命令(它用于切换当前目录,它的参数是要切换到的目录的路径,可以是绝对路径,也可以是相对路径)cd /home 进入 ‘/ home’ 目录cd .. 返回上一级目录 cd ../.. 返回上两级目录 cd 进入个人的主目录 cd ~user1 进入个人的主目录 cd - 返回上次所在的目录 pwd命令pwd 显示工作路径 ls命令查看文件与目录的命令,list之意)ls 查看目录中的文件 ls -l 显示文件和目录的详细资料 ls -a 列出全部文件,包含隐藏文件ls -R 连同子目录的内容一起列出(递归列出),等于该目录下的所有文件都会显示出来 ls [0-9] 显示包含数字的文件名和目录名 cp命令(用于复制文件,copy之意,它还可以把多个文件一次性地复制到一个目录下) -a :将文件的特性一起复制-p :连同文件的属性一起复制,而非使用默认方式,与-a相似,常用于备份-i :若目标文件已经存在时,在覆盖时会先询问操作的进行-r :递归持续复制,用于目录的复制行为-u :目标文件与源文件有差异时才会复制5. mv命令(用于移动文件、目录或更名,move之意)-f :force强制的意思,如果目标文件已经存在,不会询问而直接覆盖-i :若目标文件已经存在,就会询问是否覆盖-u :若目标文件已经存在,且比目标文件新,才会更新6. rm命令(用于删除文件或目录,remove之意)-f :就是force的意思,忽略不存在的文件,不会出现警告消息-i :互动模式,在删除前会询问用户是否操作-r :递归删除,最常用于目录删除,它是一个非常危险的参数 二、查看文件内容 cat命令(用于查看文本文件的内容,后接要查看的文件名,通常可用管道与more和less一起使用)cat file1 从第一个字节开始正向查看文件的内容 tac file1 从最后一行开始反向查看一个文件的内容 cat -n file1 标示文件的行数 more file1 查看一个长文件的内容 head -n 2 file1 查看一个文件的前两行 tail -n 2 file1 查看一个文件的最后两行 tail -n +1000 file1 从1000行开始显示,显示1000行以后的cat filename | head -n 3000 | tail -n +1000 显示1000行到3000行cat filename | tail -n +3000 | head -n 1000 从第3000行开始,显示1000(即显示3000~3999行) 三、文件搜索 find命令find / -name file1 从 ‘/‘ 开始进入根文件系统搜索文件和目录 find / -user user1 搜索属于用户 ‘user1’ 的文件和目录 find /usr/bin -type f -atime +100 搜索在过去100天内未被使用过的执行文件 find /usr/bin -type f -mtime -10 搜索在10天内被创建或者修改过的文件 whereis halt 显示一个二进制文件、源码或man的位置 which halt 显示一个二进制文件或可执行文件的完整路径删除大于50M的文件:find /var/mail/ -size +50M -exec rm {} \; 四、文件的权限 - 使用 “+” 设置权限,使用 “-“ 用于取消 chmod命令ls -lh 显示权限 chmod ugo+rwx directory1 设置目录的所有人(u)、群组(g)以及其他人(o)以读(r,4 )、写(w,2)和执行(x,1)的权限 chmod go-rwx directory1 删除群组(g)与其他人(o)对目录的读写执行权限 chown命令(改变文件的所有者)chown user1 file1 改变一个文件的所有人属性 chown -R user1 directory1 改变一个目录的所有人属性并同时改变改目录下所有文件的属性 chown user1:group1 file1 改变一个文件的所有人和群组属性 11.chgrp命令(改变文件所属用户组)chgrp group1 file1 改变文件的群组 五、文本处理 grep命令(分析一行的信息,若当中有我们所需要的信息,就将该行显示出来,该命令通常与管道命令一起使用,用于对一些命令的输出进行筛选加工等等)grep Aug /var/log/messages 在文件 ‘/var/log/messages’中查找关键词”Aug” grep ^Aug /var/log/messages 在文件 ‘/var/log/messages’中查找以”Aug”开始的词汇 grep [0-9] /var/log/messages 选择 ‘/var/log/messages’ 文件中所有包含数字的行 grep Aug -R /var/log/* 在目录 ‘/var/log’ 及随后的目录中搜索字符串”Aug” sed ‘s/stringa1/stringa2/g’ example.txt 将example.txt文件中的 “string1” 替换成 “string2” sed ‘/^$/d’ example.txt 从example.txt文件中删除所有空白行 paste命令paste file1 file2 合并两个文件或两栏的内容 paste -d ‘+’ file1 file2 合并两个文件或两栏的内容,中间用”+”区分 sort命令sort file1 file2 排序两个文件的内容 sort file1 file2 | uniq 取出两个文件的并集(重复的行只保留一份) sort file1 file2 | uniq -u 删除交集,留下其他的行 sort file1 file2 | uniq -d 取出两个文件的交集(只留下同时存在于两个文件中的文件) comm命令comm -1 file1 file2 比较两个文件的内容只删除 ‘file1’ 所包含的内容 comm -2 file1 file2 比较两个文件的内容只删除 ‘file2’ 所包含的内容 comm -3 file1 file2 比较两个文件的内容只删除两个文件共有的部分六、打包和压缩文件 tar命令(对文件进行打包,默认情况并不会压缩,如果指定了相应的参数,它还会调用相应的压缩程序(如gzip和bzip等)进行压缩和解压) -c :新建打包文件-t :查看打包文件的内容含有哪些文件名-x :解打包或解压缩的功能,可以搭配-C(大写)指定解压的目录,注意-c,-t,-x不能同时出现在同一条命令中-j :通过bzip2的支持进行压缩/解压缩-z :通过gzip的支持进行压缩/解压缩-v :在压缩/解压缩过程中,将正在处理的文件名显示出来-f filename :filename为要处理的文件-C dir :指定压缩/解压缩的目录dir压缩:tar -jcv -f filename.tar.bz2 要被处理的文件或目录名称查询:tar -jtv -f filename.tar.bz2解压:tar -jxv -f filename.tar.bz2 -C 欲解压缩的目录bunzip2 file1.bz2 解压一个叫做 ‘file1.bz2’的文件 bzip2 file1 压缩一个叫做 ‘file1’ 的文件 gunzip file1.gz 解压一个叫做 ‘file1.gz’的文件 gzip file1 压缩一个叫做 ‘file1’的文件 gzip -9 file1 最大程度压缩 rar a file1.rar test_file 创建一个叫做 ‘file1.rar’ 的包 rar a file1.rar file1 file2 dir1 同时压缩 ‘file1’, ‘file2’ 以及目录 ‘dir1’ rar x file1.rar 解压rar包zip file1.zip file1 创建一个zip格式的压缩包 unzip file1.zip 解压一个zip格式压缩包 zip -r file1.zip file1 file2 dir1 将几个文件和目录同时压缩成一个zip格式的压缩包七、系统和关机(关机、重启和登出) shutdown -h now 关闭系统(1) init 0 关闭系统(2) telinit 0 关闭系统(3) shutdown -h hours:minutes & 按预定时间关闭系统 shutdown -c 取消按预定时间关闭系统 shutdown -r now 重启(1) reboot 重启(2) logout 注销 time 测算一个命令(即程序)的执行时间八、进程相关的命令17. jps命令(显示当前系统的java进程情况,及其id号)jps(Java Virtual Machine Process Status Tool)是JDK 1.5提供的一个显示当前所有java进程pid的命令,简单实用,非常适合在linux/unix平台上简单察看当前java进程的一些简单情况。18. ps命令(用于将某个时间点的进程运行情况选取下来并输出,process之意)-A :所有的进程均显示出来-a :不与terminal有关的所有进程-u :有效用户的相关进程-x :一般与a参数一起使用,可列出较完整的信息-l :较长,较详细地将PID的信息列出ps aux # 查看系统所有的进程数据ps ax # 查看不与terminal有关的所有进程ps -lA # 查看系统所有的进程数据ps axjf # 查看连同一部分进程树状态19. kill命令(用于向某个工作(%jobnumber)或者是某个PID(数字)传送一个信号,它通常与ps和jobs命令一起使用)20. killall命令(向一个命令启动的进程发送一个信号)21. top命令是Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,类似于Windows的任务管理器。如何杀死进程:(1)图形化界面的方式(2)kill -9 pid (-9表示强制关闭)(3)killall -9 程序的名字(4)pkill 程序的名字查看进程端口号:netstat -tunlp|grep 端口号","tags":[]},{"title":"Kitex 泛化调用案例:基于 API 网关的支付开放平台","date":"2023-01-13T11:18:35.000Z","path":"2023/01/13/hz-kitex-note04/","text":"泛化调用泛化调用是不需要依赖生成代码即可对 RPC 服务发起调用的一种特性。通常用于不需要生成代码的中台服务,场景如流量中转、API 网关等。 调用方式Kitex 泛化调用目前仅支持 Thrift 协议,调用方式如下: 二进制泛化调用 HTTP 映射泛化调用 Map 映射泛化调用 JSON 映射泛化调用 其中 HTTP 映射泛化对 IDL 编写规范有专门文章介绍《Thrift-HTTP 映射的 IDL 规范》,里面详细介绍了泛化调用解析 Thrift IDL 文件整体规范、约定和已支持的注解。 IDLProviderHTTP/Map/JSON 映射的泛化调用虽然不需要生成代码,但需要使用者提供 IDL,来定义入参位置和映射关系。 目前 Kitex 有两种 IDLProvider 实现,使用者可以选择指定 IDL 路径,也可以选择传入 IDL 内容。当然也可以根据需求自行扩展 generci.DescriptorProvider。如果有 IDL 管理平台,最好与平台打通,可以及时更新 IDL。 支付开放平台支付开放平台通常是开放给服务商或商户提供收款记账等能力的服务入口,常见于支付宝、微信、银联等第三方或第四方支付渠道商,特别是前几年发展起来的聚合支付方向。 该演示项目规划要点如下: 对外暴露的是 HTTP 接口,可以用 Hertz 来做网关入口,根据 HTTP 请求使用 Kitex 泛化调用对请求分发到具体的 RPC 服务; 需要加签、验签,可以演示 Hertz 自定义 middleware; 业务服务通常有商户、支付、对账、安全等模块,业务边界清晰,为了演示仅做支付服务; 关注工程化,如 ORM、分包、代码分层、错误的统一定义及优雅处理等; 工程目录1234567891011121314151617181920212223242526272829303132333435363738394041├── Makefile├── README.md├── cmd│ └── payment│ ├── main.go│ ├── wire.go│ └── wire_gen.go├── configs│ └── sql│ └── payment.sql├── docker-compose.yaml├── docs│ └── open-payment-platform.png├── go.mod├── go.sum├── hertz-gateway│ ├── README.md│ ├── biz│ │ ├── errors│ │ │ └── errors.go│ │ ├── handler│ │ │ └── gateway.go│ │ ├── middleware│ │ │ └── gateway_auth.go│ │ ├── router│ │ │ └── register.go│ │ └── types│ │ └── response.go│ ├── main.go│ ├── router.go│ └── router_gen.go├── idl│ ├── common.thrift│ └── payment.thrift├── internal│ ├── README.md│ └── payment├── kitex_gen└── pkg └── auth └── auth.go 泛化调用的最简单实现解析IDL1provider, err := generic.NewThriftFileProvider(entry.Name(), idlPath) 构建泛化策略12//将上面解析到的 IDL 内容,根据场景需要构建 HTTP 的泛化策略g, err := generic.HTTPThriftGeneric(provider) 生成泛化调用客户端12345678910111213cli, err := genericclient.NewClient( svcName, g, client.WithResolver(nacosResolver), client.WithTransportProtocol(transport.TTHeader), client.WithMetaHandler(transmeta.ClientTTHeaderHandler),)if err != nil { hlog.Fatal(err)}// 保存映射关系handler.SvcMap[svcName] = cli 具体实现网关参数12345678{ \"sign\":\"xxx\", // 必填,签名 \"sign_type\":\"RSA\", // 必填,加签方法 \"nonce_str\":\"J84FJIUH93NFSUH894NJOF\", // 必填,随机字符串 \"merchant_id\":\"xxxx\", // 必填,用于签名验证 \"method\":\"svc-function-name\", // 必填,RPC 调用的具体方法 \"biz_params\":\"{'key':'value'}\" // 必填,RPC 业务参数} 路由规则将上述的三步构建泛化调用客户端的代码放在了 Hertz 启动服务注册路由时的实现,服务的路由规则是 /gateway/:svc,即构建 gateway 的路由组,使用参数路由知道要泛化调用 RPC 服务的具体服务名。 这部分实现可参看 route.go 文件中 registerGateway 。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051func registerGateway(r *server.Hertz) { group := r.Group(\"/gateway\").Use(middleware.GatewayAuth()...) if handler.SvcMap == nil { handler.SvcMap = make(map[string]genericclient.Client) } idlPath := \"./idl/\" c, err := os.ReadDir(idlPath) if err != nil { hlog.Fatalf(\"new thrift file provider failed: %v\", err) } nacosResolver, err := resolver.NewDefaultNacosResolver() if err != nil { hlog.Fatalf(\"err:%v\", err) } for _, entry := range c { if entry.IsDir() || entry.Name() == \"common.thrift\" { continue } svcName := strings.ReplaceAll(entry.Name(), \".thrift\", \"\") provider, err := generic.NewThriftFileProvider(entry.Name(), idlPath) if err != nil { hlog.Fatalf(\"new thrift file provider failed: %v\", err) break } //generic.JSONThriftGeneric() //将上面解析到的 IDL 内容,根据场景需要构建 HTTP 的泛化策略 g, err := generic.HTTPThriftGeneric(provider) if err != nil { hlog.Fatal(err) } cli, err := genericclient.NewClient( svcName, g, client.WithResolver(nacosResolver), client.WithTransportProtocol(transport.TTHeader), client.WithMetaHandler(transmeta.ClientTTHeaderHandler), ) if err != nil { hlog.Fatal(err) } // 保存映射关系 handler.SvcMap[svcName] = cli //路由到处理函数 group.POST(\"/:svc\", handler.Gateway) }} 发起泛化调用路由匹配成功之后,走到绑定的 handler.Gateway 处理函数即是发起泛化调用的关键点。 首先根据 handler.SvcMap,获取泛化调用客户端 genericclient.Client,然后根据路由参数 :svc 和 POST 参数 biz_params 、method 拼凑相关参数,进行泛化调用。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950svcName := c.Param(\"svc\") cli, ok := SvcMap[svcName] if !ok { c.JSON(http.StatusOK, errors.New(common.Err_BadRequest)) return } var params requiredParams if err := c.BindAndValidate(&params); err != nil { hlog.Error(err) c.JSON(http.StatusOK, errors.New(common.Err_ServerMethodNotFound)) return } req, err := http.NewRequest(http.MethodPost, \"\", bytes.NewBuffer([]byte(params.BizParams))) if err != nil { hlog.Warnf(\"new http request failed: %v\", err) c.JSON(http.StatusOK, errors.New(common.Err_RequestServerFail)) return } // 这里要留意 IDL 相关注解 req.URL.Path = fmt.Sprintf(\"/%s/%s\", svcName, params.Method) customReq, err := generic.FromHTTPRequest(req) if err != nil { hlog.Errorf(\"convert request failed: %v\", err) c.JSON(http.StatusOK, errors.New(common.Err_ServerHandleFail)) return } resp, err := cli.GenericCall(ctx, \"\", customReq) respMap := make(map[string]interface{}) if err != nil { hlog.Errorf(\"GenericCall err:%v\", err) bizErr, ok := kerrors.FromBizStatusError(err) if !ok { c.JSON(http.StatusOK, errors.New(common.Err_ServerHandleFail)) return } respMap[types.ResponseErrCode] = bizErr.BizStatusCode() respMap[types.ResponseErrMessage] = bizErr.BizMessage() c.JSON(http.StatusOK, respMap) return } realResp, ok := resp.(*generic.HTTPResponse) if !ok { c.JSON(http.StatusOK, errors.New(common.Err_ServerHandleFail)) return } realResp.Body[types.ResponseErrCode] = 0 realResp.Body[types.ResponseErrMessage] = \"ok\" c.JSON(http.StatusOK, realResp.Body) 为了更好的演示支付网关,这里做了签名验证和返回参数加签的代码。 签名首先在路由组注册时,给 /gateway 路由组注册了一个 GatewayAuth 的中间件 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748func registerGateway(r *server.Hertz) { group := r.Group(\"/gateway\").Use(middleware.GatewayAuth()...)}type AuthParam struct { Sign string `form:\"sign,required\" json:\"sign\"` SignType string `form:\"sign_type,required\" json:\"sign_type\"` MerchantId string `form:\"merchant_id,required\" json:\"merchant_id\"` NonceStr string `form:\"nonce_str,required\" json:\"nonce_str\"`}func GatewayAuth() []app.HandlerFunc { return []app.HandlerFunc{func(ctx context.Context, c *app.RequestContext) { var authParam AuthParam// TODO 签名相关的 key 或私钥应该根据商户号正确获取,这里仅做展示,没有做商户相关逻辑 key := \"123\" p, err := auth.NewSignProvider(authParam.SignType, key) if err != nil { hlog.Error(err) c.JSON(http.StatusOK, errors.New(common.Err_Unauthorized)) c.Abort() return } // 验签关键点 if !p.Verify(authParam.Sign, authParam) { hlog.Error(err) c.JSON(http.StatusOK, errors.New(common.Err_Unauthorized)) c.Abort() return } c.Next(ctx) // 响应之后加签回去 data := make(utils.H) if err = json.Unmarshal(c.Response.Body(), &data); err != nil { dataJson, _ := json.Marshal(errors.New(common.Err_RequestServerFail)) c.Response.SetBody(dataJson) return } data[types.ResponseNonceStr] = authParam.NonceStr data[types.ResponseSignType] = authParam.SignType data[types.ResponseSign] = p.Sign(data) dataJson, _ := json.Marshal(data) c.Response.SetBody(dataJson) }}} 项目优化错误处理在网关和 RPC 服务都要演示错误处理,可能数量比较多。为了规范实现,把错误定义收拢到 IDL 公共协议中去,根据生成的代码返回特定的错误,便于判断和管理。 错误定义在 idl 目录中新增了 common.thrift 文件,把错误码都枚举出来,并约定不同的服务或地方使用不同的错误码段。 12345678910111213141516171819namespace go commonenum Err{ // gateway 10001- 19999 BadRequest = 10001, Unauthorized = 10002, ServerNotFound = 10003, ServerMethodNotFound = 10004, RequestServerFail = 10005, ServerHandleFail = 10006, ResponseUnableParse = 10007, // payment 20001- 29999 DuplicateOutOrderNo = 20001, // other 30001- 93999 Errxxx = 30001,} 在网关处的错误进行了简单的封装,方便使用: 12345678910111213141516type Err struct { ErrCode int64 `json:\"err_code\"` ErrMsg string `json:\"err_msg\"`}// New Error, the error_code must be defined in IDL.func New(errCode common.Err) Err { return Err{ ErrCode: int64(errCode), ErrMsg: errCode.String(), }}func (e Err) Error() string { return e.ErrMsg} 用例: 123456import ( \"github.com/cloudwego/biz-demo/open-payment-platform/hertz-gateway/biz/errors\" \"github.com/cloudwego/biz-demo/open-payment-platform/kitex_gen/common\")c.JSON(http.StatusOK, errors.New(common.Err_RequestServerFail)) RPC 服务使用 Kitex 业务异常 的特性支持,只需要在泛化调用客户端和 RPC 服务端制定好相关配置即可。 具体用法如: 123456import ( \"github.com/cloudwego/biz-demo/open-payment-platform/kitex_gen/common\")// 这里类型转换较为繁琐,亦可考虑如何简化优化封装// 比如一个思路是如果想业务异常也想不依赖某个框架用法,如何做return nil, kerrors.NewBizStatusError(int32(common.Err_DuplicateOutOrderNo), common.Err_DuplicateOutOrderNo.String()) 遗留问题 该项目没有演示配置相关的使用,所以注册中心和数据库配置仅是硬编码; 签名处理如何获取商户私钥或 key ,需要实际业务考虑; 错误处理可继续优化; 泛化调用注解示例较为简单,可根据实际入参和映射关系进行灵活配置; 整洁架构在业务膨胀之后是否会遇到新的问题; 关于作者 笔名:Damon,技术爱好者,微服务架构设计,云原生、容器化技术,现从事Go相关,涉及云原生、边缘计算、AI人工智能、云产品Devops落地实践等云原生技术。拿过专利。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注 AI绘画扫我 星球","tags":[{"name":"后端","slug":"后端","permalink":"http://damon008.github.io/tags/%E5%90%8E%E7%AB%AF/"},{"name":"Go","slug":"Go","permalink":"http://damon008.github.io/tags/Go/"},{"name":"kitex","slug":"kitex","permalink":"http://damon008.github.io/tags/kitex/"}]},{"title":"kitex 基于 K8s 的服务注册与发现","date":"2022-11-07T08:48:35.000Z","path":"2022/11/07/hz-kitex-note03/","text":"概览在很久之前的文章中说过,K8s 作为云原生时代的创造者,下一代云原生的中间利器,从云原生 1.0 到 2.0,作为基石,成就无数服务畅游每一台机器。前面的文章 KiteX 入门篇 介绍了如何简单的开发一个高性能 RPC 微服务,并且我们看到其性能与吞吐还是不错的。但需要中间件 Nacos 来连接服务端与客户端。今天,我们主要从 K8s 角度来看 Kitex 如何接入云原生,甚至后面的 Istio。 实践服务端前面文章 KiteX 入门篇,我们介绍了如何创建一个服务端,那我们这次改造下,让其接入 K8s。 这里主要现需要去掉关于 Nacos 的逻辑,然后我们再看下面的代码: 1server.WithServiceAddr(&net.TCPAddr{Port: 9000}), 这段代码的含义是通过 Nacos 注册时候,我们把服务注册的端口为 9000,但如果服务是以 K8s 部署的 Pod 形式,则代表的是 Pod 的端口,同时,由于 K8s 这种开源注册中心默认使用 TCP 协议,所以这里支持的是 TCP 协议。这样简单的配置,即可让服务注册到 K8s,被 k8s-api 发现。 完整代码如下: 1234567891011121314151617181920212223svr := note.NewServer( new(api.NoteApi), //基于svc进行服务注册,9000为svc的port server.WithServiceAddr(&net.TCPAddr{Port: 9000}), server.WithMuxTransport(), server.WithLimit(&limit.Option{MaxConnections: 10000, MaxQPS: 5000}), server.WithPayloadCodec(thrift.NewThriftCodecWithConfig(thrift.FastRead | thrift.FastWrite)), //server.WithTracer(prometheus.NewServerTracer(\":9092\", \"/kitexNoteserver\")), server.WithErrorHandler(func(err error) error { error := errno.ConvertErr(err) return error }), server.WithCodec(codec.NewDefaultCodecWithSizeLimit(1024 * 1024 * 10)),//10M server.WithMetaHandler(transmeta.ServerTTHeaderHandler), // registry ) err := svr.Run() if err != nil { klog.Fatal(err) } 部署脚本由于我们需要通过镜像部署服务,所以需要 dockerfile: 12345678910111213141516171819202122232425262728293031323334353637FROM ubuntu:16.04 as buildRUN apt-get update && apt-get install -y --no-install-recommends \\ g++ \\ ca-certificates \\ wget && \\ rm -rf /var/lib/apt/lists/*ENV GOLANG_VERSION 1.18.1RUN wget -nv -O - https://studygolang.com/dl/golang/go1.18.1.linux-amd64.tar.gz \\ | tar -C /usr/local -xzENV GOPROXY=https://goproxy.cn,directENV GO111MODULE=onENV GOPATH /goENV PATH $GOPATH/bin:/usr/local/go/bin:$PATHRUN apt-get update && apt-get install -y gitWORKDIR /go/srcCOPY . .#RUN go env -w GOPROXY=https://goproxy.cn,direct#RUN go env -w GO111MODULE=onRUN go build -o hello-server ./server/helloRUN chmod +x ./hello-serverRUN rm -rf cache && rm -rf kitex_gen && rm -rf README.md\\&& rm -rf build && rm -rf codec && rm -rf client \\&& rm -rf images && rm -rf idl && rm -rf go.mod && rm -rf go.sum \\&& rm -rf pkg && rm -rf script && rm -rf retry \\&& rm -rf server && rm -rf streaming && rm -rf LICENSECMD [\"./hello-server\"] 然后我们需要 K8s 的 yaml: 12345678910111213141516171819202122232425262728apiVersion: apps/v1kind: Deploymentmetadata: name: hello-service-deployment namespace: default labels: app: hello-servicespec: replicas: 3 selector: matchLabels: app: hello-service template: metadata: labels: app: hello-service spec: nodeSelector: hello-service: \"true\" containers: - name: node-service image: node-service imagePullPolicy: IfNotPresent ports: - name: hello01 containerPort: 9000 ...... 最后需要对这个服务进行创建 svc,以便进行负载均衡被其它服务所访问。 123456789101112apiVersion: v1kind: Servicemetadata: name: hello-service-svc namespace: defaultspec: ports: - name: hello01 port: 9000 targetPort: hello01 selector: app: hello-service 此时,一个完整的服务端的代码以及部署脚本就完结了。 客户端同样的,客户端我们来看看看,也是很简单,同样先需去掉 Nacos 的配置,然后我们引入利用 K8s svc 进行服务的请求: 1client.WithHostPorts(\"hello-service-svc.default.svc.cluster.local:9000\"), 在创建客户端的时候,客户端的 host 要写实际集群中的内网地址:hello-service-svc.default.svc.cluster.local,这样就不用再搭配第三方的服务注册中心了。 这样就可以通过该 host 进行访问服务端的服务了。。。 部署脚本客户端的部署脚本类似服务端的,同样需要部署的构建镜像脚本、部署 deployment 脚本、部署 svc。此处客户端由于是通过容器部署,所以我们需要把其服务的 port 进行映射到主机,这样方便来进行浏览器访问服务: 1234567891011121314apiVersion: v1kind: Servicemetadata: name: customer-service-svc namespace: defaultspec: type: NodePort ports: - name: customer01 nodePort: 30230 port: 3000 targetPort: customer01 selector: app: customer-service 测试我们先通过命令进行部署: 1kubectl create -f ... 创建完服务相关资源后,我们看看 pod: 资源 svc 情况: 我们把 port 映射到主机上,然后我们通过浏览器进行访问客户端,此处我们访问的是存在 10000 条数据入库的服务: 我们可以看到,数据大概是 9634 条总数,我们取 500 条,耗时 40ms,接下来,我们换作取 5000 条: 数据大小 624kb,发现大概耗时:159ms,还是可以的。 最后,我们看看拿全部数据大概耗时: 发现全部数据大小:1.2M,耗时 300 多毫秒,还是不错的。 特性我们的服务为什么能如此之快呢,这里我们引入了一种高性能网络库:netpoll,以及在编解码方面,我们利用 Thrift 的 IDL 进行生成代码,同时,利用其提供的 FastRead、FastWrite 来进行快速传输。 如有想要源码的,可以关注后,后台联系博主获取哟~ 结束福利 开源实战利用 k8s 作微服务的架构设计代码: https://gitee.com/damon_one/spring-cloud-k8s 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,微服务架构设计,云原生、容器化技术,现从事Go相关,涉及云原生、边缘计算、AI人工智能、云产品Devops落地实践等云原生技术。拿过专利。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注 AI绘画扫我 星球","tags":[{"name":"后端","slug":"后端","permalink":"http://damon008.github.io/tags/%E5%90%8E%E7%AB%AF/"},{"name":"Go","slug":"Go","permalink":"http://damon008.github.io/tags/Go/"},{"name":"kitex","slug":"kitex","permalink":"http://damon008.github.io/tags/kitex/"}]},{"title":"入门 kitex 基础篇","date":"2022-11-02T12:41:00.000Z","path":"2022/11/02/hz-kitex-note02/","text":"概览KiteX 是 bytedance 开源的高性能 RPC 框架,实现了高吞吐、高负载、高性能等居多特性,具体请看 KiteX 的实践,文章介绍多传输协议、消息协议时,说到 KiteX 支持的协议类型:Thrift、Protobuf 等,今天我们主要来实践如何利用 KiteX 基于对应的 IDL 生成对应协议的代码。 Thrift 简介Thrift 本身是一软件框架(远程过程调用框架),用来进行可扩展且跨语言的服务的开发。它结合了功能强大的软件堆栈和代码生成引 擎,以构建在 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml 这些编程语言间无缝结合的、高效的服务。同时,作为 IDL(接口定义语言 Interface Definition Language),允许你定义一个简单的定义文件中的数据类型和服务接口,以作为输入文件,编译器生成代码用来方便地生成 RPC 客户端和服务器通信的无缝跨编程语言。 Protobuf 简介Protobuf 全称是 Google Protocol Buffer,是一种高效轻便的结构化数据存储方式,用于数据的通信协议、数据存储等。相对比 XML 来说,其特点: 语言无关,平台无关 高效 扩展性、兼容性更强 基于 IDL 的 KiteX 实践在 RPC 框架中,我们知道,服务端与客户端通信的前提是远程通信,但这种通信又存在一种关联,那就是通过一套相关的协议(消息、通信、传输等)来规范,但客户端又不用关心底层的技术实现,只要定义好了这种通信方式即可。 在 KiteX 中,也提供了一种生成代码的命令行工具:kitex,目前支持 thrift、protobuf 等 IDL,并且支持生成一个服务端项目的骨架。 安装命令行工具环境 如果您之前未搭建 Golang 开发环境, 可以参考 Golang 安装 推荐使用最新版本的 Golang,我们保证最新三个正式版本的兼容性(现在 >= v1.16)。 确保打开 go mod 支持 (Golang >= 1.15 时,默认开启) kitex 暂时没有针对 Windows 做支持,如果本地开发环境是 Windows 建议使用 WSL2 安装 确保 GOPATH 环境变量已经被正确地定义(例如 export GOPATH=~/go)并且将 $GOPATH/bin添加到 PATH 环境变量之中(例如 export PATH=$GOPATH/bin:$PATH);请勿将 GOPATH 设置为当前用户没有读写权限的目录 安装 kitex:go install github.com/cloudwego/kitex/tool/cmd/kitex@latest 安装 thriftgo:go install github.com/cloudwego/thriftgo@latest 安装成功后,执行 kitex --version 可以看到如下信息: 12$ kitex --versionv0.4.2 如果在安装阶段发生问题,可能主要是由于对 Golang 的不当使用造成的,需要逐一排查。 编写一个 IDL我们先新建一个项目:hz-kitex-examples,新建完之后,我们在该项目下新建一个目录:idl,然后我们编写一个 thrift IDL: 接下来,我们编写这个 IDL: 123456789101112131415161718192021222324252627282930namespace go hellostruct ReqBody { 1: string name 2: i32 type 3: string email}struct Request { 1: string data 2: string message 3: ReqBody reqBody}struct Msg { 1: i64 status 2: i64 code 3: string msg}struct Response { 1: Msg msg 2: string data}service HelloService { Response echo(1: Request req) Response testHello4Get(1: Request req) Response testHello4Post(1: Request req)} 这里我们定义了一个命名空间:hello,这个是代表生成的代码中有一个目录:hello,然后我们编写一个请求对象:ReqBody,接着定义一个泛对象,包括了那个请求对象,这块没要求,自己定义好就行,同时我们定义了响应对象Response,此外,我们还定义了一个类,类中存在三个函数方法。 生成代码在定义完 IDL 后,我们来看如何生成代码呢?直接执行如下命令: 1kitex -module \"hz-kitex-examples\" -thrift frugal_tag -service helloserver idl/hello.thrift 这里有几个参数 tag: -module module_name 该参数用于指定生成代码所属的 Go 模块,会影响生成代码里的 import path。 如果当前目录是在 $GOPATH/src 下的一个目录,那么可以不指定该参数;kitex 会使用 $GOPATH/src 开始的相对路径作为 import path 前缀。例如,在 $GOPATH/src/example.com/hello/world 下执行 kitex,那么 kitex_gen/example_package/example_package.go 在其他代码代码里的 import path 会是 example.com/hello/world/kitex_gen/example_package。 如果当前目录不在 $GOPATH/src 下,那么必须指定该参数。 如果指定了 -module 参数,那么 kitex 会从当前目录开始往上层搜索 go.mod 文件 如果不存在 go.mod 文件,那么 kitex 会调用 go mod init 生成 go.mod; 如果存在 go.mod 文件,那么 kitex 会检查 -module 的参数和 go.mod 里的模块名字是否一致,如果不一致则会报错退出; 最后,go.mod 的位置及其模块名字会决定生成代码里的 import path。 -service service_name 使用该选项时,kitex 会生成构建一个服务的脚手架代码,参数 service_name 给出启动时服务自身的名字,通常其值取决于使用 Kitex 框架时搭配的服务注册和服务发现功能。 对于当前项目,我们执行如下: 1kitex -module \"hz-kitex-examples\" -thrift frugal_tag -service helloserver idl/hello.thrift 由于当前项目不在环境路径下,需要指定 go.mod 所在的目录模块的名称,同时,我们指定一个服务名。 这样在执行后,我们会发现生成的目录结构如下图: 在生成的目录中根目录是kitex_gen,代表是 kitex 工具生成的,其次其目录下有一个 hello 目录,这是代表 IDL 文件中的 ns,在其下面有一个文件:定义了请求对象与响应对象的序列化、传输信息的读写等操作。 在其下面还存在一个 service 目录,用来生成跟客户端与服务端相关的 service 处理逻辑。其中也定义了 service 中处理的方法信息。 同时,我们可以看到生成了服务端的基础框架: 服务端新建一个server 目录,然后在里面新建一个项目hello,此时把生成服务端的骨架代码拷贝到里面: 拷贝完之后,我们可以丰满服务端的函数的逻辑,以 Echo 函数为例: 1234567891011121314151617181920212223242526func (s *HelloApi) Echo(ctx context.Context, req *api.Request) (resp *api.Response, err error) { klog.Info(\"hello service enter: \" + GetIpAddr2()) resp = &api.Response { Msg: &api.Msg { Status: 200, Code: 10000, Msg: req.Message, }, Data: req.Message, } return resp, nil}func GetIpAddr2() string { conn, err := net.Dial(\"udp\", \"8.8.8.8:53\") if err != nil { klog.Error(err) return \"\" } localAddr := conn.LocalAddr().(*net.UDPAddr) // 192.168.1.20:61085 ip := strings.Split(localAddr.String(), \":\")[0] return ip} 然后再定义启动函数: 12345678910111213141516svr := hello.NewServer(new(api.HelloApi), server.WithServiceAddr(&net.TCPAddr{Port: 2008}), server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: constants.HelloServiceName}), server.WithPayloadCodec(thrift.NewThriftCodecWithConfig(thrift.FastRead | thrift.FastWrite)), server.WithErrorHandler(func(err error) error { error := errno.ConvertErr(err) return error }), //指定默认 Codec 的包大小限制,默认无限制 option: codec.NewDefaultCodecWithSizeLimit server.WithCodec(codec.NewDefaultCodecWithSizeLimit(1024 * 1024 * 10)),//10M server.WithLimit(&limit.Option{MaxConnections: 10000, MaxQPS: 5000}), //连接多路复用(mux) server.WithMuxTransport(), server.WithMetaHandler(transmeta.ServerTTHeaderHandler), server.WithRegistry(registry.NewNacosRegistry(r1)), ) 在这里,我们定义了服务端的端口:2008,同时被注册到 Nacos。启动之后: 可以看到被注册到 Nacos: 这里注册 Nacos 的代码前面已经讲过了,具体可以看:KiteX 的实践。 客户端关于客户端,也是一样,新建一个 client 目录,里面新建一个项目customer-service,新建启动 main 函数,这里与服务端类似不再赘述了。主要注意一点:这里由于需要提供 Http 协议接口,需要结合 Hertz 来进行: 至于 Hertz,它是一个高性能的 Http 微服务框架在后面的文章中会进一步讲解,此处不再赘述。 启动函数新建完后,我们需要初始化一个 RPC 连接的客户端: 此处客户端的初始化,也是基于之前生成的代码: 在这个函数初始化客户端时,需要定义请求的服务名、网络库、负载均衡策略、出错误处理机制等。同时,我们还需要复写需要调用的函数,去调用相关的接口: 写完 RPC 的部分,一个简单的 RPC 协议调用就能串联起来了,此时,我们来简单写下客户端的接口调用: 1234567891011func HelloDemo(ctx context.Context, c *app.RequestContext) { req := &api.Request{Message: \"my request\"} // TODO resp, err := rpc.Echo(context.Background(), req) if err != nil { log.Fatal(err) } klog.Info(resp) // TODO c.JSON(consts.StatusOK, (resp))} 上面定义的是客户端的接口入口,进入后,会调用 rpc 的部分。同时在调用 RPC 前后,可以有自己的逻辑处理以及响应数据的处理,在 TODO 部分。 启动客户端进行服务注册: 测试在启动完服务端、客户端后,我们访问客户端的 http 接口: 1http://192.168.6.51:3000/v1/hello/test 我们再多几次进行访问: 发现其访问的性能以及速度还是不错的,这得益于 KiteX 框架中使用了自研的 Netpoll 网络库以及实现了高效的吞吐编解码性能提升。 关于作者 笔名:Damon,技术爱好者,微服务架构设计,云原生、容器化技术,现从事Go相关,涉及云原生、边缘计算、AI人工智能、云产品Devops落地实践等云原生技术。拿过专利。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注 AI绘画扫我 星球","tags":[{"name":"后端","slug":"后端","permalink":"http://damon008.github.io/tags/%E5%90%8E%E7%AB%AF/"},{"name":"Go","slug":"Go","permalink":"http://damon008.github.io/tags/Go/"},{"name":"kitex","slug":"kitex","permalink":"http://damon008.github.io/tags/kitex/"}]},{"title":"基于高性能 RPC 框架 Kitex 实现微服务实战","date":"2022-10-16T02:35:15.000Z","path":"2022/10/16/hz-kitex-note01/","text":"概览Kitex 字节跳动内部的 Golang 微服务 RPC 框架,具有高性能、强可扩展的特点,在字节内部已广泛使用。如果对微服务性能有要求,又希望定制扩展融入自己的治理体系,Kitex 会是一个不错的选择。 架构设计 框架特点 高性能 使用自研的高性能网络库 Netpoll,性能相较 go net 具有显著优势。 扩展性 提供了较多的扩展接口以及默认扩展实现,使用者也可以根据需要自行定制扩展,具体见下面的框架扩展。 多消息协议 RPC 消息协议默认支持 Thrift、Kitex Protobuf、gRPC。Thrift 支持 Buffered 和 Framed 二进制协议;Kitex Protobuf 是 Kitex 自定义的 Protobuf 消息协议,协议格式类似 Thrift;gRPC 是对 gRPC 消息协议的支持,可以与 gRPC 互通。除此之外,使用者也可以扩展自己的消息协议。 多传输协议 传输协议封装消息协议进行 RPC 互通,传输协议可以额外透传元信息,用于服务治理,Kitex 支持的传输协议有 TTHeader、HTTP2。TTHeader 可以和 Thrift、Kitex Protobuf 结合使用;HTTP2 目前主要是结合 gRPC 协议使用,后续也会支持 Thrift。 多种消息类型 支持 PingPong、Oneway、双向 Streaming。其中 Oneway 目前只对 Thrift 协议支持,双向 Streaming 只对 gRPC 支持,后续会考虑支持 Thrift 的双向 Streaming。 服务治理 支持服务注册/发现、负载均衡、熔断、限流、重试、监控、链路跟踪、日志、诊断等服务治理模块,大部分均已提供默认扩展,使用者可选择集成。 代码生成 Kitex 内置代码生成工具,可支持生成 Thrift、Protobuf 以及脚手架代码。 实战基于 Etcd 实现服务注册与发现 首先基于 Windows 环境,搭建 Go 开发环境,安装需要的开发工具。 安装需要的软件工具:Etcd、Nacos。 新建 RPC 微服务项目服务提供者新建一工程项目,引入依赖: 1234github.com/cloudwego/hertz v0.3.2github.com/cloudwego/kitex v0.4.2github.com/kitex-contrib/registry-etcd v0.0.0-20220110034026-b1c94979cea3github.com/kitex-contrib/registry-nacos v0.0.1 这里除了引入基于 Http 协议的框架 Hertz,还需要引入 Kitex 依赖,同时,需要引入注册中心服务:etcd、nacos 等。 引入完之后,我们需要新建基于 Hertz 的项目,因为此项目是基于 Hertz(此框架后续会进行相关的开发延伸),实际是基于 Http 协议,新建完成后,我们来看 main 函数: 12345678910111213r, err := etcd.NewEtcdRegistry([]string{\"127.0.0.1:2379\"})svr := user.NewServer(new(UserServiceImpl), server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: constants.UserServiceName}), // server name server.WithMiddleware(middleware.CommonMiddleware), // middleware server.WithMiddleware(middleware.ServerMiddleware), server.WithServiceAddr(addr), // address server.WithLimit(&limit.Option{MaxConnections: 1000, MaxQPS: 100}), // limit server.WithMuxTransport(), // Multiplex server.WithSuite(trace.NewDefaultServerSuite()), // tracer server.WithBoundHandler(bound.NewCpuLimitHandler()), // BoundHandler server.WithRegistry(r),) 由于此处使用的是 Etcd 作为服务注册中心,故 Kitex 提供了此模块etcd \"github.com/kitex-contrib/registry-etcd\",etcd.NewEtcdRegistry即可配置基于 Etcd 的注册,同时,此服务作为服务提供者,通过server.WithRegistry(r)即可实现注册于 etcd。 此处的完整代码如下: 服务消费者同样,消费者采用的 Hertz 框架进行,此处不再赘述: 但在请求生产者之前,都是需要进行服务的发现:这里基于 RPC: 1234567891011121314151617r, err := etcd.NewEtcdResolver([]string{constants.EtcdAddress})c, err := noteservice.NewClient( constants.NoteServiceName, client.WithMiddleware(middleware.CommonMiddleware), client.WithInstanceMW(middleware.ClientMiddleware), client.WithMuxConnection(1), // mux client.WithRPCTimeout(3*time.Second), // rpc timeout client.WithConnectTimeout(50*time.Millisecond), // conn timeout client.WithFailureRetry(retry.NewFailurePolicy()), // retry client.WithSuite(trace.NewDefaultClientSuite()), // tracer client.WithResolver(r), // resolver ) if err != nil { panic(err) } noteClient = c 此时,我们在消费者端,新增请求生产者的客户端时,我们需要把请求对象constants.NoteServiceName告诉其客户端。 这样就实现了服务的发现,同时,服务发现后需要进行相关的接口调用,此处: 这些需要在 RPC 中实现,此处不再赘述,后续会加入基于 Kitex 的完整开发示例。 此时,一个完整的服务注册与发现就实现完毕了。此处基于 Etcd,接下来,基于 Nacos 就简单多了。 基于 Nacos 实现服务注册与发现前面我们实现了基于 Etcd 的服务注册与发现,接下来,我们基于 Nacos 来实现,简单点,就是把所有基于 Etcd 的地方改成基于 Nacos: 12//r, err := etcd.NewEtcdResolver([]string{constants.EtcdAddress})r1,err := resolver.NewDefaultNacosResolver() 但此处需要主要,Nacos 的函数不是以 nacos 进行命名,而是registry.NewDefaultNacosRegistry()或resolver.NewDefaultNacosResolver()。 最后,可以启动相关服务: 注册的服务: 关于作者 笔名:Damon,技术爱好者,微服务架构设计,云原生、容器化技术,现从事Go相关,涉及云原生、边缘计算、AI人工智能、云产品Devops落地实践等云原生技术。拿过专利。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注 AI绘画扫我 星球","tags":[{"name":"后端","slug":"后端","permalink":"http://damon008.github.io/tags/%E5%90%8E%E7%AB%AF/"},{"name":"Go","slug":"Go","permalink":"http://damon008.github.io/tags/Go/"},{"name":"hertz","slug":"hertz","permalink":"http://damon008.github.io/tags/hertz/"},{"name":"kitex","slug":"kitex","permalink":"http://damon008.github.io/tags/kitex/"}]},{"title":"单机Java极致高性能优化","date":"2022-08-11T07:43:56.000Z","path":"2022/08/11/core-java03/","text":"CPU 密集型操作,顾名思义就是需要持续依赖 CPU 资源来执行的操作,比如各种逻辑计算、解析、判断等等。在这种情况下,我们的优化方向是尽可能地利用多核 CPU 资源,并且避免让 CPU 做无效的切换,因为 CPU 已经在不停地工作了,谁来干都一样,同时切换 CPU 还浪费资源。所以这个时候,我们最好让任务线程数和 CPU 核数保持一致,从而最大限度地利用 CPU 资源。 和 CPU 密集型操作相对的,就是 IO 密集型操作了,比如磁盘 IO 或者网络 IO,这个过程操作系统会挂起任务线程,让出 CPU 资源。此时如果任务线程较少,同时 IO 时间相对较长,那可能会出现所有线程都被挂起,然后 CPU 资源都在闲着的情况,所以此时我们需要适当地增加任务线程数量,来提高吞吐量,同时将 CPU 资源利用起来。 那为什么要说这个呢?因为这是做程序优化的基本原则。通过前面课程的学习,我们知道,秒杀系统里有提供两种类型的服务,一个是 Web 服务,一个是 RPC 服务,前者一般提供 HTTP 接口,后者提供 RPC 接口。当然这两种服务我们一般都是通过 Tomcat 来启动发布,但它们两者之间还是有些不同的。Web 服务接受和处理请求走的是 Tomcat 那套线程模型,而 RPC 服务则是根据选择的 RPC 框架的不同而有所变化,所以这节课我们首先来了解一下 Tomcat 相关的知识。 Tomcat根据我们以往“知己知彼”的学习方式,先看下 Tomcat 在 NIO 线程模型下是怎么工作的,简图如下所示: 简单来说就是: Tomcat 启动时,会创建一个 Server 端的 Socket,来监控我们配置的端口号; 之后使用一个 Acceptor 来接受请求,然后将请求放到一个 Poller 下的事件队列中; Poller 会轮询取出事件队列中的 Channel,并将其注册到自身下的 Selector; 而 Selector 也会不停轮询检查就绪的 Channel,然后将其交给 Tomcat 线程池; Tomcat 线程池会拿出一个线程来进行处理,包括解析请求头、请求体等,并将其封装进 HttpServletRequest; 最后执行自定义的 Servlet 业务逻辑,执行完毕将响应结果返回。 所以从上图可以看出,所谓的非阻塞,其实就是相对以前的 BIO,Tomcat 不再是用一个线程将一个请求从头处理到尾,而是分阶段来执行了。好处显然易见,那就是提高了系统吞吐量。 在了解了 Tomcat 基本原理之后,我们再回过头来看下有什么地方是我们可以入手优化的。先看下 Tomcat 给我们开放了哪些可配置项: 1<Connector port=\"8080\" protocol=\"HTTP/1.1\" connectionTimeout=\"20000\" redirectPort=\"8443\" /> 上面是 Tomcat 的 Connector 默认配置,首先是端口号,其次是 protocol,也就是上面说到的线程模型。Tomcat 8 之后默认使用的都是 NIO 模式,这个也可以通过我们服务的启动来查看: 如上图所示,就代表分别使用的是 NIO 模式和 NIO2(AIO) 模式,当然还可以选择 BIO 模式以及 APR 模式。具体对比可参考下表: 那说完线程模型的选择,从上图中我们可以看到有个 Tomcat 线程池的概念,它是通过哪些配置来控制的呢?这里我们只摘几个重要的配置说一下,详细信息如下表所示: 说完了 Tomcat 的配置,这里再简单说说 Servlet 的部分知识。我们都知道 Servlet 从 3.0 开始加入了异步,从 3.1 开始又新增了对 IO 非阻塞的支持,那么这个和 Tomcat 线程模型中提到的异步非阻塞是一个概念吗?这里我们就来捋一捋。 首先从上面的 Tomcat 线程模型图中,我们可以清晰地看到,NIO 或 AIO 的概念是针对请求的接收来说,而 Servlet 的异步非阻塞主要是针对请求的处理,已经是到了 Tomcat 线程池那里了。 我们先来看下 Servlet3.0 前后的变化对比,如下图所示: 概述一下就是,Servlet3.0 之前,Tomcat 线程在执行自定义 Servlet 时,如果过程中发生了 IO,那么 Tomcat 线程只能在那等着结果,这时线程是被挂起的,如果被挂起的多了,自然会影响对其他请求的处理。 所以在 Servlet3.0 之后,支持在这种情况下将这种等待的任务交给一个自定义的业务线程池去做,这样 Tomcat 线程可以很快地回到线程池,处理其他请求。而业务线程在执行完业务逻辑以后,通过调用指定的方法,告诉 Tomcat 线程池接下来可以将业务线程执行的结果返回给调用方,这样就实现了同步转异步的效果。 这样做的好处,可能对提高系统的吞吐量有一定帮助,但从 JVM 层面来说,并没有减少工作量。业务线程在执行任务遇到 IO 时,依然会阻塞,现在只是由业务线程池代替了 Tomcat 线程池做了最耗时的那部分工作,这样也许可以将原来的 200 个 Tomcat 线程,拆分成 20 个 Tomcat 线程、180 个业务线程来配合工作。这里原生 Servlet 以及 SpringMVC 对异步功能支持的测试代码,你可以看 GitHub 代码库中的 AsyncServlet 类和 TestAsyncController 类,相信你一看就明白了。 接着我们再聊一下 Servlet3.1 的非阻塞,这块简单来说,就是针对请求消息体的读取,这是个 IO 过程,以前是阻塞式读取,现在支持非阻塞读取了。实现的大致原理就是在读取数据时,新增一个监听事件,在读取完成后由 Tomcat 线程执行回调。 在了解了 Tomcat 线程模型之后,我们接着再说下 RPC 框架相关的知识。 RPC 框架虽然 RPC 服务处理请求的过程,会依据选用的 RPC 框架而有所不同,但绝大部分 RPC 框架底层使用的都是 Netty,而 Netty 则是基于 NIO 开发的一种网络通信框架,支持多种通信协议,其服务端线程模型简略图如下所示: 简单描述就是: 在服务启动时,会创建一个 Server 端 Socket,监控我们配置的端口号; 然后将 NioServerSocketChannel 注册到 Boss Pool 中的一个 Selector 上; 再之后对 Selector 做轮询,将就绪状态的连接封装成 NioSocketChannel 并注册到 Worker Pool 下的一个 Selector 上; 而 Worker Pool 下的 Selector 也是同样轮询,找出可读和可写状态的分别执行不同操作。 同时两个 Pool 中都有任务队列,是不同场景下用户自定义或外部通过特定方式提交过去的任务,都会被依次执行。 所以当我们的应用只提供 RPC 服务时,我们可以将 Tomcat 的核心线程池配置,也就是 minSpareThreads 配置成 1,因为用不到。而我们主要需要调整的是 RPC 框架的相关配置,以 Dubbo 为例,我们看下 dubbo:protocol 的主要配置项: 在 Netty 中,虽然只有一个 Worker Pool,但会做两种类型的事情,一个是做 IO 处理,包括请求消息的读写,另一个是做业务逻辑处理。 而 Dubbo 将其分成了两个线程池,也就是上面表格中的两个线程池配置。这两个线程池做的事情,会根据 Dispatcher 的配置而有所不同。Netty 是以事件驱动的形式来工作的,像请求、响应、连接、断开、异常等操作都是事件;而 Dubbo 中的 Dispatcher 就是将不同的事件类型分给不同的线程池来处理,如果你感兴趣的话可以去看下 Dubbo 中 WrappedChannelHandler 类的 5 个实现类,分别对应 Dispatcher 的 5 个选项。 最后一个配置项 Queues,这个默认值是 0,也就是不接受等待,如果没有空闲线程处理任务,将会直接返回。这个得和客户端配置配合使用,如果这里配置了 0,那客户端最好配置重试。 讲完了两种服务的底层线程模型之后,我们再来介绍一下静态资源相关的优化。 静态资源我们知道在秒杀系统中,客户端与服务端既有动态数据交互,也有静态数据交互,而我们做系统优化有个基本的原则,即前后端交互越少,数据越小,链路越短,数据离用户越近,响应就越快。 基于这个原则,针对以上的静态数据,我们就可以把静态文件 CDN 化,资源前移到全国各地的 CDN 节点上,用户秒杀的时候就近进行下载,就不需要都挤到中心的 Tomcat 服务器上了。 静态资源前移,大家平常也会做,感受比较深的是不是就是客户端的页面加载更快了,但除了性能的提升外,其实它对系统稳定也至关重要。 试想一下,当几百万人同时来拉取这些较大的资源文件时,对中心的 Tomcat 服务器以及公司的网络带宽都是巨大的压力。京东当初在进行口罩抢购的时候,这些静态资源就差点把公司的出口带宽打满,影响交易大盘,后来紧急扩容才避免了危机。 另外,这些静态资源对 Tomcat 所在物理机的网卡挑战也很大,京东在资源 CDN 化前,物理机的万兆网卡曾被打满,后来经过优化之后,网卡的流量只有原来的 10% 了。 在最后,我们再说下 Java 运行的基础环境,JVM 相关的知识以及优化。 JVM这里如果你对一些基本概念,比如 JVM 内存结构、GC 原理、垃圾收集器类型等还不了解,那建议你先了解一下,会有事半功倍的效果。这块的内容比较多,又比较重要,但我们没办法一一展开,只说最核心的优化点。 先看个 JVM 内存模型以及常用配置,如下图所示: 其实针对 JVM 的优化,我们最关心的无非就两个问题,一个是垃圾回收器怎么选择,另一个就是对选择的垃圾回收器如何做优化。这里我们分别讲一下。 对于垃圾回收器的选择,是需要分业务场景的。如果我们提供的服务对响应时间敏感,并且堆内存能够给到 8G 以上的,那建议选择 G1;堆内存较小或 JDK 版本较低的,可以选择 CMS。相反如果对响应时间不敏感,追求一定的吞吐量的,则建议选择 ParallelGC,同时这也是 JDK8 的默认垃圾回收器。 选择完垃圾回收器之后,接下来就针对不同的垃圾回收器,分别做不同的参数优化。 首先是 ParallelGC,其主要配置参数如下: 然后是 CMS,在 ParallelGC 配置参数的基础上增加以下配置: 再说下 G1 的优化配置(在使用了 G1 的情况下,就不要设置 -Xmn 和 XX:NewRatio 了),同样是在 ParallelGC 配置参数的基础上增加以下配置: 因为我们秒杀的业务场景更适合选择 G1 来做垃圾回收器,那这里也给一个在 8 核 16G 容器下的 JVM 配置,具体如下: 1-Xms8192m -Xmx8192m -XX:MaxMetaspaceSize=512m -XX:+UseG1GC -XX:ParallelGCThreads=8 -XX:ConcGCThreads=4 -XX:G1HeapRegionSize=8m 总结对于 Tomcat 的优化,在秒杀的特定业务场景下针对线程模型的选择,从理论和实际压测上看,NIO2 比 NIO 是有吐吞量的提升,但不是很大,如果为了省事,选择默认的 NIO 即可。而 APR 的话,因为我们静态资源都上到 CDN 了,并且 Web 服务并不直接对外(请求由 Nginx 转发过来),也不要求是 HTTPS 方式,所以这里也不考虑了,和线程池相关的配置,最好按照这节课中的建议做适当的调整。 同时我们也提到了 Servlet 在 3.0 和 3.1 版本提供的异步非阻塞功能,由于秒杀的接口入参不涉及文件之类的较大消息体,所以 IO 非阻塞可以不用。而异步功能这块,其实可以有更好的选择,那就是 Vertx 技术,这也是我们在下节课中,将会单独介绍的一种异步化编程思想技术。 而对于 RPC 框架,我们主要介绍了基于 NIO 开发的一种网络通信框架 Netty,了解了 Netty 主要使用两个池子,即使用 Boss Pool 和 Worker Pool 来实现 Reactor 模式。同时选择了一个具体的 RPC 框架 Dubbo,来做了详细的配置优化讲解。 在聊完了两种服务的底层线程模型与优化后,我们介绍了静态资源的优化方案,即将静态资源上到 CDN,以减轻对秒杀域名流量的压力,同时可以依靠 CDN 的全国部署,快速加载到对应的静态资源。 另外,我们还提到了 Java 运行的环境 JVM,包括垃圾回收器的选择与优化,即如果我们提供的服务对响应时间敏感,并且堆内存能够给到 8G 以上的,那就选择 G1;而堆内存较小或 JDK 版本较低的,可以选择 CMS。相反如果对响应时间不敏感,追求一定的吞吐量的,则建议选择 ParallelGC。同时针对不同的垃圾回收器,也给出了对应的优化配置。","tags":[]},{"title":"表过大如何优化","date":"2022-08-11T07:35:33.000Z","path":"2022/08/11/Mysql-03/","text":"通过优化数据类型、合理增加冗余字段、拆分表和使用非空约束等方法,来改进表的设计,从而提高查询性能。 数据类型优化在改进表的设计时,首先可以考虑优化字段的数据类型。下面我就来讲解 2 种方法,一种是针对整数类型数据,尽量使用小的整数类型来定义;另外一种是,如果字段既可以用文本类型,也可以用整数类型,尽量使用整数类型。 先说第一种方法,对整数类型数据进行优化。 遇到整数类型的字段可以用 INT 型。这样做的理由是,INT 型数据有足够大的取值范围,不用担心数据超出取值范围的问题。刚开始做项目的时候,首先要保证系统的稳定性,这样设计字段类型是可以的。 但是,随着你的经验越来越丰富,参与的项目越来越大,数据量也越来越多的时候,你就不能只从系统稳定性的角度来思考问题了,还要考虑到系统整体的效率。 这是因为,在数据量很大的时候,数据类型的定义,在很大程度上会影响到系统整体的执行效率。这个时候,你就必须同时考虑稳定性和效率。 第 2 种优化方法,就是既可以使用文本类型也可以使用整数类型的字段,要使用整数类型,而不要用文本类型。 跟文本类型数据相比,大整数往往占用更少的存储空间,因此,在存取和比对的时候,可以占用更少的内存。所以,遇到既可以使用文本类型,又可以使用整数类型来定义的字段,尽量使用整数类型,这样可以提高查询的效率。 在 demo.test1 中,我给商品编号设定的数据类型是 MEDIUMINT,给流水唯一编号设定的数据类型是 BIGINT。 这样设定的原因是,MEDIUMINT 类型的取值范围是“无符号数 0 – 16777215”。对于商品编号来说,其实够用了。我的 400 万条数据中没有超过这个范围的值。而流水唯一编号是一个长度为 18 位的数字,用字符串数据类型 TEXT 肯定是可以的,大整数类型 BIGINT 的取值范围是“无符号数 0 – 18446744083709551616”,有 20 位,所以,用大整数类型数据来定义流水唯一编号,也是可以的。 原来,INT 类型占用 4 个字节存储空间,而 MEDIUMINT 类型只占用 3 个字节的存储空间,比 INT 类型节省了 25% 的存储空间。demo.test1 的第一个字段的数据类型是 MEDIUMINT,demo.test 的第一个字段的数据类型是 INT。因此,我们来对比下两个表的第一个字段 ,demo.test1 占用的存储空间就比 demo.test 节省了 25%。 再来看看这两个表的第二个字段:流水唯一编号 transuniqueid。在 demo.test 中,这个字段的类型是 TEXT,而 TEXT 类型占用的字节数等于“实际字符串长度 + 2”,在咱们的这个场景中,流水唯一编号的长度是 18,所占用的存储空间就是 20 个字节。在 demo.test1 中,流水唯一编号的数据类型是 BIGINT,占用的存储空间就是 8 个字节。这样一来,demo.test1 在第二个字段上面占用的存储空间就比 demo.test 节省了(20-8)÷20=60%。很明显,对于流水唯一编号字段,demo.test1 比 demo.test 更加节省空间。 因此,我建议你,遇到数据量大的项目时,一定要在充分了解业务需求的前提下,合理优化数据类型,这样才能充分发挥资源的效率,使系统达到最优。 合理增加冗余字段以提高效率在数据量大,而且需要频繁进行连接的时候,为了提升效率,我们也可以考虑增加冗余字段来减少连接。 不过,你要注意的一点是,这样一来,商品流水表中包含了一个冗余字段“商品名称”,不但存储空间变大了,而且,如果某个商品名称做了修改,一定要对应修改流水表里的商品名称。否则,就会出现两个表里的商品名称不一致的情况。 所以,在实际的工作场景中,你需要权衡增加冗余字段的利与弊。这里给你一个建议:增加冗余字段一定要符合 2 个条件,第一个是,这个冗余字段不需要经常进行修改;第二个是,这个冗余字段查询的时候不可或缺。只有满足这两个条件,才可以考虑增加冗余字段,否则就不值得增加这个冗余字段了。 拆分表跟刚刚的在表中增加冗余字段的方法相反,拆分表的思路是,把 1 个包含很多字段的表拆分成 2 个或者多个相对较小的表。 这样做的原因是,这些表中某些字段的操作频率很高,经常要进行查询或者更新操作,而另外一些字段的使用频率却很低,如果放在一个表里面,每次查询都要读取大记录,会消耗较多的资源。 这个时候,如果把这个大表拆分开,把使用频率高的字段放在一起形成一个表,把剩下的使用频率低的字段放在一起形成一个表,这样查询操作每次读取的记录比较小,查询效率自然也就提高了。 使用非空约束在设计字段的时候,如果业务允许,我建议你尽量使用非空约束。这样做的好处是,可以省去判断是否为空的开销,提高存储效率。而且,非空字段也容易创建索引。使用非空约束,甚至可以节省存储空间(每个字段 1 个比特)。 这样一来,我们就省去了判断空值的开销,还能够节省一些存储空间。 总结 修改数据类型以节省存储空间; 在利大于弊的情况下增加冗余字段; 把大表中查询频率高的字段和查询频率低的字段拆分成不同的表; 尽量使用非空约束。 但是,我要提醒你的是,这些方法都是有利有弊的,比如,修改数据类型,节省存储空间的同时,你要考虑到数据不能超过取值范围;增加冗余字段的时候,不要忘了确保数据一致性;把大表拆分,也意味着你的查询会增加新的连接,从而增加额外的开销和运维的成本。因此,你一定要结合实际的业务需求进行权衡。","tags":[]},{"title":"K8s亲和性问题","date":"2022-08-09T09:33:38.000Z","path":"2022/08/09/k8s-affinity/","text":"kubernetes默认调度器的调度过程调度过程如下: 预选(Predicates) 优选(Priorities) 选定(Select) 节点亲和性和pod亲和性的区别举个例子,假设给小明分配班级(小明是pod,班级是节点) 节点亲和性:直接告诉小明,你去一年级 pod亲和性:从小朋友中找出和小明同年的,找到了小张,发现小张是一年级的,于是让小明去一年级 节点亲和性:硬亲和性 requiredDuringSchedulinglgnoredDuringExecution:用于定义节点硬亲和性 nodeSelectorTerm:节点选择器,可以有多个,之间的关系是逻辑或,即一个nodeSelectorTerm满足即可 matchExpressions:匹配规则定义,多个之间的关系是逻辑与,即同一个nodeSelectorTerm下所有matchExpressions定义的规则都匹配,才算匹配成功 1234567891011121314apiVersion: v1kind: Podmetadata: name: with-required-nodeaffinityspec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - {key: zone, operator: In, values: [\"foo\"]} containers: - name: nginx image: nginx 功能与nodeSelector类似,用的是匹配表达式,可以被理解为新一代节点选择器 不满足硬亲和性条件时,pod为Pending状态 在预选阶段,节点硬亲和性被用于预选策略MatchNodeSelector 节点亲和性:软亲和性特点:条件不满足时也能被调度 1234567891011121314151617181920212223242526272829apiVersion: apps/v1kind: Deploymentmetadata: name: myapp-deploy-with-node-affinityspec: replicas: 3 selector: matchLabels: app: nginx template: metadata: name: nginx labels: app: nginx spec: affinity: nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 60 preference: matchExpressions: - {key: zone, operator: In, values: [\"foo\"]} - weight: 30 preference: matchExpressions: - {key: ssd, operator: Exists, values: []} containers: - name: nginx image: nginx 集群中的节点,由于标签不同,导致的优先级结果如下: 在优选阶段,节点软亲和性被用于优选函数NodeAffinityPriority 注意:NodeAffinityPriority并非决定性因素,因为优选阶段还会调用其他优选函数,例如SelectorSpreadPriority(将pod分散到不同节点以分散节点故障导致的风险) pod副本数增加时,分布的比率会参考节点亲和性的权重 Pod亲和性(podAffinity) 如果需求是:新增的pod要和已经存在pod(假设是A)在同一node上,此时用节点亲和性是无法完成的,因为A可能和节点没啥关系(可能是随机调度的),此时只能用pod亲和性来实现 pod亲和性:一个pod与已经存在的某个pod的亲和关系,需要通过举例来说明 创建一个deployment,这个pod有标签app=tomcat: 1kubectl run tomcat -l app=tomcat --image tomcat:alpine 创建pod,需求是和前面的pod在一起,使用pod亲和性来实现: 123456789101112131415apiVersion: v1kind: Podmetadata: name: with-pod-affinity-1spec: affinity: podAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - {key: app, operator: In, values: [\"tomcat\"]} topologyKey: kubernetes.io/hostname containers: - name: nginx image: nginx 调度逻辑: 123A[1. 用matchExpressions的规则app=tomcat搜索] -->B(2. 找到tomcat的pod,也就确定了该pod的节点,假设是A节点)B --> C(3. topologyKey是kubernetes.io/hostname,所以去找A节点kubernetes.io/hostname标签的值,假设是xxx)C --> D(4. 将新的pod调度到kubernetes.io/hostname=xxx的节点) 硬亲和:requiredDuringSchedulingIgnoredDuringExecution软亲和:preferredDuringSchedulingIgnoredDuringExecution Pod反亲和(podAntiAffinity) 与亲和性相反,将当前pod调度到满足匹配条件之外的节点上 适用场景: 分散同一类应用 将不同安全级别的pod调度至不同节点 示例如下,匹配表达式和自身标签一致,作用是分散同一类应用,让相同pod不要调度到同一个节点: 12345678910111213141516171819202122232425apiVersion: apps/v1kind: Deploymentmetadata: name: myapp-with-pod-anti-affinityspec: replicas: 4 selector: matchLabels: app: myapp template: metadata: name: myapp labels: app: myapp spec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - {key: app, operator: In, values: [\"myapp\"]} topologyKey: kubernetes.io/hostname containers: - name: nginx image: nginx 如果集群中只有三个节点,那么执行上述yaml的结果就是最多创建三个pod,另一个始终处于pending状态 参考本篇笔记参考了以下文章,两张图片也来自该文章,致敬作者 1https://mp.weixin.qq.com/s/AaiX_7j97_V-TeIiUBU73Q","tags":[]},{"title":"Java基础","date":"2022-08-09T07:27:14.000Z","path":"2022/08/09/core-java/","text":"JVM类加载类加载过程加载->验证(符合jvm规范)->准备(分配内存空间)->解析(符号引用替换为直接引用)->初始化(赋值的代码初始化时执行)->使用->卸载 啥时候去加载一个类?当代码中使用到这个类的时候 JVM进程启动后被加载 双亲委派机制:先找父亲去加载,没找到的话,就下推到给儿子,在其负责的目录中的类中找。启动类加载器-扩展类加载器-应用程序类加载器-自定义类加载器 双亲委派机制双亲委派机制定义:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。 简单说下实现流程: 12345671. 首先判断该类是否已经被加载2.该类未被加载,如果父类不为空,交给父类加载3.如果父类为空,交给bootstrap classloader 加载4.如果类还是无法被加载到,则触发findclass,抛出classNotFoundException(findclass这个方法当前只有一个语句,就是抛出classNotFoundException),如果想自己实现类加载器的话,可以继承classLoader后重写findclass方法,加载对应的类) 启动JVM进程后,自带一个垃圾回收的后台线程 TomcatTomcat自定义了Common、Catalina、Shared等类加载器,其实为了加载其自身的核心基础类库。然后tomcat为每个部署在里面的Web应用都用一个对应的WebApp类加载器,负责加载我们部署的应用的类。 每个WebApp负责加载自己对应的那个web应用的class文件,不会传导给上层类加载器去加载。 StringString类型String 是引用类型,最为显著的一个特点就是它具有恒定不变性,但是值传递,传递的是地址的值,所有的变量都可以说是值传递,就看是什么类型,引用类型,传递的值是地址的值,值类型传递的是变量的赋值。string是引用类型,只是编译器对其做了特殊处理。 字符串拼接即使使用 + 号作为字符串的拼接,也一样可以被编译器优化成 StringBuilder 的方式。但再细致些,你会发现在编译器优化的代码中,每次循环都会生成一个新的 StringBuilder 实例,同样也会降低系统的性能。 所以平时做字符串拼接的时候,我建议你还是要显示地使用 String Builder 来提升系统性能。 如果在多线程编程中,String 对象的拼接涉及到线程安全,你可以使用 StringBuffer。但是要注意,由于 StringBuffer 是线程安全的,涉及到锁竞争,所以从性能上来说,要比 StringBuilder 差一些。 如何使用 String.intern 节省内存使用 String.intern 来节省内存空间,从而优化 String 对象的存储。 具体做法就是,在每次赋值的时候使用 String 的 intern 方法,如果常量池中有相同值,就会重复使用该对象,返回对象引用,这样一开始的对象就可以被回收掉。这种方式可以使重复性非常高的地址信息存储大小从 20G 降到几百兆。 12345678910SharedLocation sharedLocation = new SharedLocation();sharedLocation.setCity(messageInfo.getCity().intern()); sharedLocation.setCountryCode(messageInfo.getRegion().intern());sharedLocation.setRegion(messageInfo.getCountryCode().intern());Location location = new Location();location.set(sharedLocation);location.set(messageInfo.getLongitude());location.set(messageInfo.getLatitude()); 在字符串常量中,默认会将对象放入常量池;在字符串变量中,对象是会创建在堆内存中,同时也会在常量池中创建一个字符串对象,String 对象中的 char 数组将会引用常量池中的 char 数组,并返回堆内存对象引用。 如果调用 intern 方法,会去查看字符串常量池中是否有等于该对象的字符串的引用,如果没有,在 JDK1.6 版本中会复制堆中的字符串到常量池中,并返回该字符串引用,堆内存中原有的字符串由于没有引用指向它,将会通过垃圾回收器回收。 在 JDK1.7 版本以后,由于常量池已经合并到了堆中,所以不会再复制具体字符串了,只是会把首次遇到的字符串的引用添加到常量池中;如果有,就返回常量池中的字符串引用。 在一开始字符串”abc”会在加载类时,在常量池中创建一个字符串对象。 1234567String a =new String(\"abc\").intern();String b = new String(\"abc\").intern();if(a==b) {//true System.out.print(\"a==b\");} 创建 a 变量时,调用 new Sting() 会在堆内存中创建一个 String 对象,String 对象中的 char 数组将会引用常量池中字符串。在调用 intern 方法之后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用。 创建 b 变量时,调用 new Sting() 会在堆内存中创建一个 String 对象,String 对象中的 char 数组将会引用常量池中字符串。在调用 intern 方法之后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用。 而在堆内存中的两个对象,由于没有引用指向它,将会被垃圾回收。所以 a 和 b 引用的是同一个对象。 而在堆内存中的两个对象,由于没有引用指向它,将会被垃圾回收。所以 a 和 b 引用的是同一个对象。 如果在运行时,创建字符串对象,将会直接在堆内存中创建,不会在常量池中创建。所以动态创建的字符串对象,调用 intern 方法,在 JDK1.6 版本中会去常量池中创建运行时常量以及返回字符串引用,在 JDK1.7 版本之后,会将堆中的字符串常量的引用放入到常量池中,当其它堆中的字符串对象通过 intern 方法获取字符串对象引用时,则会去常量池中判断是否有相同值的字符串的引用,此时有,则返回该常量池中字符串引用,跟之前的字符串指向同一地址的字符串对象。 使用 intern 方法需要注意的一点是,一定要结合实际场景。因为常量池的实现是类似于一个 HashTable 的实现方式,HashTable 存储的数据越大,遍历的时间复杂度就会增加。如果数据过大,会增加整个字符串常量池的负担。 如何使用字符串的分割方法最后我想跟你聊聊字符串的分割,这种方法在编码中也很最常见。Split() 方法使用了正则表达式实现了其强大的分割功能,而正则表达式的性能是非常不稳定的,使用不恰当会引起回溯问题,很可能导致 CPU 居高不下。 所以我们应该慎重使用 Split() 方法,我们可以用 String.indexOf() 方法代替 Split() 方法完成字符串的分割。如果实在无法满足需求,你就在使用 Split() 方法时,对回溯问题加以重视就可以了。 Stream如何提高遍历集合效率List 集合类,那我想你一定也知道集合的顶端接口 Collection。在 Java8 中,Collection 新增了两个流方法,分别是 Stream() 和 parallelStream() Stream在 Java8 之前,我们通常是通过 for 循环或者 Iterator 迭代来重新排序合并数据,又或者通过重新定义 Collections.sorts 的 Comparator 方法来实现,这两种方式对于大数据量系统来说,效率并不是很理想。 Java8 中添加了一个新的接口类 Stream,他和我们之前接触的字节流概念不太一样,Java8 集合中的 Stream 相当于高级版的 Iterator,他可以通过 Lambda 表达式对集合进行各种非常便利、高效的聚合操作(Aggregate Operation),或者大批量数据操作 (Bulk Data Operation)。 Stream 的聚合操作与数据库 SQL 的聚合操作 sorted、filter、map 等类似。我们在应用层就可以高效地实现类似数据库 SQL 的聚合操作了,而在数据操作方面,Stream 不仅可以通过串行的方式实现数据操作,还可以通过并行的方式处理大批量数据,提高数据的处理效率。 替换for循环或迭代循环的实现,Stream API 进行实现: 串行实现: 1Map<String, List<Student>> stuMap = stuList.stream().filter((Student s) -> s.getHeight() > 160) .collect(Collectors.groupingBy(Student ::getSex)); 并行实现: 1Map<String, List<Student>> stuMap = stuList.parallelStream().filter((Student s) -> s.getHeight() > 160) .collect(Collectors.groupingBy(Student ::getSex)); 通过上面两个简单的例子,我们可以发现,Stream 结合 Lambda 表达式实现遍历筛选功能非常得简洁和便捷。 Stream 是如何优化遍历的 我们通常还会将中间操作称为懒操作,也正是由这种懒操作结合终结操作、数据源构成的处理管道(Pipeline),实现了 Stream 的高效。 深入浅出HashMap的设计与优化常用的数据结构 数组:采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为 O(1),但在数组中间以及头部插入数据时,需要复制移动后面的元素。 链表:一种在物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素)组成,结点可以在运行时动态生成。每个结点都包含“存储数据单元的数据域”和“存储下一个结点地址的指针域”这两个部分。由于链表不用必须按顺序存储,所以链表在插入的时候可以达到 O(1) 的复杂度,但查找一个结点或者访问特定编号的结点需要 O(n) 的时间。 哈希表:根据关键码值(Key value)直接进行访问的数据结构。通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做哈希函数,存放记录的数组就叫做哈希表。 树:由 n(n≥1)个有限结点组成的一个具有层次关系的集合,就像是一棵倒挂的树。 HashMap 的实现结构作为最常用的 Map 类,它是基于哈希表实现的,继承了 AbstractMap 并且实现了 Map 接口。 哈希表将键的 Hash 值映射到内存地址,即根据键获取对应的值,并将其存储到内存地址。也就是说 HashMap 是根据键的 Hash 值来决定对应值的存储位置。通过这种索引方式,HashMap 获取数据的速度会非常快。 但也会有新的问题。如果再来一个 (y,“bb”),哈希函数 f(y) 的哈希值跟之前 f(x) 是一样的,这样两个对象的存储地址就冲突了,这种现象就被称为哈希冲突。那么哈希表是怎么解决的呢?方式有很多,比如,开放定址法、再哈希函数法和链地址法。 开放定址法很简单,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把 key 存放到冲突位置后面的空位置上去。这种方法存在着很多缺点,例如,查找、扩容等,所以我不建议你作为解决哈希冲突的首选。 再哈希法顾名思义就是在同义词产生地址冲突时再计算另一个哈希函数地址,直到冲突不再发生,这种方法不易产生“聚集”,但却增加了计算时间。如果我们不考虑添加元素的时间成本,且对查询元素的要求极高,就可以考虑使用这种算法设计。 HashMap 则是综合考虑了所有因素,采用链地址法解决哈希冲突问题。这种方法是采用了数组(哈希表)+ 链表的数据结构,当发生哈希冲突时,就用一个链表结构存储相同 Hash 值的数据。 HashMap 添加元素优化在 JDK1.8 中,HashMap 引入了红黑树数据结构来提升链表的查询效率。 这是因为链表的长度超过 8 后,红黑树的查询效率要比链表高,所以当链表超过 8 时,HashMap 就会将链表转换为红黑树,这里值得注意的一点是,这时的新增由于存在左旋、右旋效率会降低。讲到这里,我前面我提到的“因链表过长而导致的查询时间复杂度高”的问题,也就迎刃而解了。 HashMap 获取元素优化当 HashMap 中只存在数组,而数组中没有 Node 链表时,是 HashMap 查询数据性能最好的时候。一旦发生大量的哈希冲突,就会产生 Node 链表,这个时候每次查询元素都可能遍历 Node 链表,从而降低查询数据的性能。 特别是在链表长度过长的情况下,性能将明显降低,红黑树的使用很好地解决了这个问题,使得查询的平均复杂度降低到了 O(log(n)),链表越长,使用黑红树替换后的查询效率提升就越明显。 我们在编码中也可以优化 HashMap 的性能,例如,重写 key 值的 hashCode() 方法,降低哈希冲突,从而减少链表的产生,高效利用哈希表,达到提高性能的效果。 HashMap 扩容优化HashMap 也是数组类型的数据结构,所以一样存在扩容的情况。 在 JDK1.7 中,HashMap 整个扩容过程就是分别取出数组元素,一般该元素是最后一个放入链表中的元素,然后遍历以该元素为头的单向链表元素,依据每个被遍历元素的 hash 值计算其在新数组中的下标,然后进行交换。这样的扩容方式会将原来哈希冲突的单向链表尾部变成扩容后单向链表的头部。 而在 JDK 1.8 中,HashMap 对扩容操作做了优化。由于扩容数组的长度是 2 倍关系,所以对于假设初始 tableSize = 4 要扩容到 8 来说就是 0100 到 1000 的变化(左移一位就是 2 倍),在扩容中只用判断原来的 hash 值和左移动的一位(newtable 的值)按位与操作是 0 或 1 就行,0 的话索引不变,1 的话索引变成原索引加上扩容前数组。 之所以能通过这种“与运算“来重新分配索引,是因为 hash 值本来就是随机的,而 hash 按位与上 newTable 得到的 0(扩容前的索引位置)和 1(扩容前索引位置加上扩容前数组长度的数值索引处)就是随机的,所以扩容的过程就能把之前哈希冲突的元素再随机分布到不同的索引中去。 总结HashMap 通过哈希表数据结构的形式来存储键值对,这种设计的好处就是查询键值对的效率高。 我们在使用 HashMap 时,可以结合自己的场景来设置初始容量和加载因子两个参数。当查询操作较为频繁时,我们可以适当地减少加载因子;如果对内存利用率要求比较高,我可以适当的增加加载因子。 我们还可以在预知存储数据量的情况下,提前设置初始容量(初始容量 = 预知数据量 / 加载因子)。这样做的好处是可以减少 resize() 操作,提高 HashMap 的效率。 HashMap 还使用了数组 + 链表这两种数据结构相结合的方式实现了链地址法,当有哈希值冲突时,就可以将冲突的键值对链成一个链表。 但这种方式又存在一个性能问题,如果链表过长,查询数据的时间复杂度就会增加。HashMap 就在 Java8 中使用了红黑树来解决链表过长导致的查询性能下降问题。以下是 HashMap 的数据结构图: 实际应用中,我们设置初始容量,一般得是 2 的整数次幂。你知道原因吗? 2的幂次方减1后每一位都是1,让数组每一个位置都能添加到元素。例如十进制8,对应二进制1000,减1是0111,这样在&hash值使数组每个位置都是可以添加到元素的,如果有一个位置为0,那么无论hash值是多少那一位总是0,例如0101,&hash后第二位总是0,也就是说数组中下标为2的位置总是空的。如果初始化大小设置的不是2的幂次方,hashmap也会调整到比初始化值大且最近的一个2的幂作为capacity。 1)通过将 Key 的 hash 值与 length-1 进行 & 运算,实现了当前 Key 的定位,2 的幂次方可以减少冲突(碰撞)的次数,提高 HashMap 查询效率; 2)如果 length 为 2 的次幂,则 length-1 转化为二进制必定是 11111…… 的形式,在于 h 的二进制与操作效率会非常的快,而且空间不浪费;如果 length 不是 2 的次幂,比如 length 为 15,则 length-1 为 14,对应的二进制为 1110,在于 h 与操作,最后一位都为 0,而 0001,0011,0101,1001,1011,0111,1101 这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费。 网络通信优化之I/O模型:如何解决高并发下I/O瓶颈在计算机中,流是一种信息的转换。流是有序的,因此相对于某一机器或者应用程序而言,我们通常把机器或者应用程序接收外界的信息称为输入流(InputStream),从机器或者应用程序向外输出的信息称为输出流(OutputStream),合称为输入 / 输出流(I/O Streams)。 不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢? 我们知道字符到字节必须经过转码,这个过程非常耗时,如果我们不知道编码类型就很容易出现乱码问题。所以 I/O 流提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。 传统 I/O 的性能问题1. 多次内存复制在传统 I/O 中,我们可以通过 InputStream 从源数据中读取数据流输入到缓冲区里,通过 OutputStream 将数据输出到外部设备(包括磁盘、网络)。 JVM 会发出 read() 系统调用,并通过 read 系统调用向内核发起读请求; 内核向硬件发送读指令,并等待读就绪; 内核把将要读取的数据复制到指向的内核缓存中; 操作系统内核将数据复制到用户空间缓冲区,然后 read 系统调用返回。 在这个过程中,数据先从外部设备复制到内核空间,再从内核空间复制到用户空间,这就发生了两次内存复制操作。这种操作会导致不必要的数据拷贝和上下文切换,从而降低 I/O 的性能。 2. 阻塞在传统 I/O 中,InputStream 的 read() 是一个 while 循环操作,它会一直等待数据读取,直到数据就绪才会返回。这就意味着如果没有数据就绪,这个读取操作将会一直被挂起,用户线程将会处于阻塞状态。 在少量连接请求的情况下,使用这种方式没有问题,响应速度也很高。但在发生大量连接请求时,就需要创建大量监听线程,这时如果线程没有数据就绪就会被挂起,然后进入阻塞状态。一旦发生线程阻塞,这些线程将会不断地抢夺 CPU 资源,从而导致大量的 CPU 上下文切换,增加系统的性能开销。 如何优化 I/O 操作 使用缓冲区优化读写流操作 NIO 与传统 I/O 不同,它是基于块(Block)的,它以块为基本单位处理数据。在 NIO 中,最为重要的两个组件是缓冲区(Buffer)和通道(Channel)。Buffer 是一块连续的内存块,是 NIO 读写数据的中转地。Channel 表示缓冲数据的源头或者目的地,它用于读取缓冲或者写入数据,是访问缓冲的接口。 传统 I/O 和 NIO 的最大区别就是传统 I/O 是面向流,NIO 是面向 Buffer。Buffer 可以将文件一次性读入内存再做后续处理,而传统的方式是边读文件边处理数据。虽然传统 I/O 后面也使用了缓冲块,例如 BufferedInputStream,但仍然不能和 NIO 相媲美。使用 NIO 替代传统 I/O 操作,可以提升系统的整体性能,效果立竿见影。 使用 DirectBuffer 减少内存复制 NIO 的 Buffer 除了做了缓冲块优化之外,还提供了一个可以直接访问物理内存的类 DirectBuffer。普通的 Buffer 分配的是 JVM 堆内存,而 DirectBuffer 是直接分配物理内存 (非堆内存)。 DirectBuffer 则是直接将步骤简化为数据直接保存到非堆内存,从而减少了一次数据拷贝。 由于 DirectBuffer 申请的是非 JVM 的物理内存,所以创建和销毁的代价很高。DirectBuffer 申请的内存并不是直接由 JVM 负责垃圾回收,但在 DirectBuffer 包装类被回收时,会通过 Java Reference 机制来释放该内存块。 DirectBuffer 只优化了用户空间内部的拷贝,而之前我们是说优化用户空间和内核空间的拷贝,那 Java 的 NIO 中是否能做到减少用户空间和内核空间的拷贝优化呢?答案是可以的,DirectBuffer 是通过 unsafe.allocateMemory(size) 方法分配内存,也就是基于本地类 Unsafe 类调用 native 方法进行内存分配的。而在 NIO 中,还存在另外一个 Buffer 类:MappedByteBuffer,跟 DirectBuffer 不同的是,MappedByteBuffer 是通过本地类调用 mmap 进行文件内存映射的,map() 系统调用方法会直接将文件从硬盘拷贝到用户空间,只进行一次数据拷贝,从而减少了传统的 read() 方法从硬盘拷贝到内核空间这一步。 避免阻塞,优化 I/O 操作 NIO 发布后,通道和多路复用器这两个基本组件实现了 NIO 的非阻塞。 通道(Channel):Channel 有自己的处理器,可以完成内核空间和磁盘之间的 I/O 操作。在 NIO 中,我们读取和写入数据都要通过 Channel,由于 Channel 是双向的,所以读、写可以同时进行。 多路复用器(Selector):Selector 是 Java NIO 编程的基础。用于检查一个或多个 NIO Channel 的状态是否处于可读、可写。 Selector 是基于事件驱动实现的,我们可以在 Selector 中注册 accpet、read 监听事件,Selector 会不断轮询注册在其上的 Channel,如果某个 Channel 上面发生监听事件,这个 Channel 就处于就绪状态,然后进行 I/O 操作。 一个线程使用一个 Selector,通过轮询的方式,可以监听多个 Channel 上的事件。我们可以在注册 Channel 时设置该通道为非阻塞,当 Channel 上没有 I/O 操作时,该线程就不会一直等待了,而是会不断轮询所有 Channel,从而避免发生阻塞。 目前操作系统的 I/O 多路复用机制都使用了 epoll,相比传统的 select 机制,epoll 没有最大连接句柄 1024 的限制。所以 Selector 在理论上可以轮询成千上万的客户端。","tags":[]},{"title":"Linux 文件系统是怎么工作的","date":"2022-08-09T07:21:17.000Z","path":"2022/08/09/linux-03/","text":"索引节点和目录项文件系统,本身是对存储设备上的文件,进行组织管理的机制。组织方式不同,就会形成不同的文件系统。 在 Linux 中一切皆文件。不仅普通的文件和目录,就连块设备、套接字、管道等,也都要通过统一的文件系统来管理。 为了方便管理,Linux 文件系统为每个文件都分配两个数据结构,索引节点(index node)和目录项(directory entry)。它们主要用来记录文件的元信息和目录结构。 索引节点,简称为 inode,用来记录文件的元数据,比如 inode 编号、文件大小、访问权限、修改日期、数据的位置等。索引节点和文件一一对应,它跟文件内容一样,都会被持久化存储到磁盘中。所以记住,索引节点同样占用磁盘空间。 目录项,简称为 dentry,用来记录文件的名字、索引节点指针以及与其他目录项的关联关系。多个关联的目录项,就构成了文件系统的目录结构。不过,不同于索引节点,目录项是由内核维护的一个内存数据结构,所以通常也被叫做目录项缓存。 换句话说,索引节点是每个文件的唯一标志,而目录项维护的正是文件系统的树状结构。目录项和索引节点的关系是多对一,你可以简单理解为,一个文件可以有多个别名。 第一,目录项本身就是一个内存缓存,而索引节点则是存储在磁盘中的数据。在前面的 Buffer 和 Cache 原理中,我曾经提到过,为了协调慢速磁盘与快速 CPU 的性能差异,文件内容会缓存到页缓存 Cache 中。 第二,磁盘在执行文件系统格式化时,会被分成三个存储区域,超级块、索引节点区和数据块区。其中, 超级块,存储整个文件系统的状态。 索引节点区,用来存储索引节点。 数据块区,则用来存储文件数据。 虚拟文件系统目录项、索引节点、逻辑块以及超级块,构成了 Linux 文件系统的四大基本要素。不过,为了支持各种不同的文件系统,Linux 内核在用户进程和文件系统的中间,又引入了一个抽象层,也就是虚拟文件系统 VFS(Virtual File System)。 VFS 定义了一组所有文件系统都支持的数据结构和标准接口。这样,用户进程和内核中的其他子系统,只需要跟 VFS 提供的统一接口进行交互就可以了,而不需要再关心底层各种文件系统的实现细节。 这里,我画了一张 Linux 文件系统的架构图,帮你更好地理解系统调用、VFS、缓存、文件系统以及块存储之间的关系。 通过这张图,你可以看到,在 VFS 的下方,Linux 支持各种各样的文件系统,如 Ext4、XFS、NFS 等等。按照存储位置的不同,这些文件系统可以分为三类。 第一类是基于磁盘的文件系统,也就是把数据直接存储在计算机本地挂载的磁盘中。常见的 Ext4、XFS、OverlayFS 等,都是这类文件系统。 第二类是基于内存的文件系统,也就是我们常说的虚拟文件系统。这类文件系统,不需要任何磁盘分配存储空间,但会占用内存。我们经常用到的 /proc 文件系统,其实就是一种最常见的虚拟文件系统。此外,/sys 文件系统也属于这一类,主要向用户空间导出层次化的内核对象。 第三类是网络文件系统,也就是用来访问其他计算机数据的文件系统,比如 NFS、SMB、iSCSI 等。 这些文件系统,要先挂载到 VFS 目录树中的某个子目录(称为挂载点),然后才能访问其中的文件。拿第一类,也就是基于磁盘的文件系统为例,在安装系统时,要先挂载一个根目录(/),在根目录下再把其他文件系统(比如其他的磁盘分区、/proc 文件系统、/sys 文件系统、NFS 等)挂载进来。 文件系统I/O把文件系统挂载到挂载点后,你就能通过挂载点,再去访问它管理的文件了。VFS 提供了一组标准的文件访问接口。这些接口以系统调用的方式,提供给应用程序使用。 就拿 cat 命令来说,它首先调用 open() ,打开一个文件;然后调用 read() ,读取文件的内容;最后再调用 write() ,把文件内容输出到控制台的标准输出中: 123int open(const char *pathname, int flags, mode_t mode);ssize_t read(int fd, void *buf, size_t count);ssize_t write(int fd, const void *buf, size_t count); 文件读写方式的各种差异,导致 I/O 的分类多种多样。最常见的有,缓冲与非缓冲 I/O、直接与非直接 I/O、阻塞与非阻塞 I/O、同步与异步 I/O 等。 接下来,我们就详细看这四种分类。 第一种,根据是否利用标准库缓存,可以把文件 I/O 分为缓冲 I/O 与非缓冲 I/O。 缓冲 I/O,是指利用标准库缓存来加速文件的访问,而标准库内部再通过系统调度访问文件。 非缓冲 I/O,是指直接通过系统调用来访问文件,不再经过标准库缓存。 注意,这里所说的“缓冲”,是指标准库内部实现的缓存。比方说,你可能见到过,很多程序遇到换行时才真正输出,而换行前的内容,其实就是被标准库暂时缓存了起来。 无论缓冲 I/O 还是非缓冲 I/O,它们最终还是要经过系统调用来访问文件。而根据上一节内容,我们知道,系统调用后,还会通过页缓存,来减少磁盘的 I/O 操作。 第二,根据是否利用操作系统的页缓存,可以把文件 I/O 分为直接 I/O 与非直接 I/O。 直接 I/O,是指跳过操作系统的页缓存,直接跟文件系统交互来访问文件。 非直接 I/O 正好相反,文件读写时,先要经过系统的页缓存,然后再由内核或额外的系统调用,真正写入磁盘。 想要实现直接 I/O,需要你在系统调用中,指定 O_DIRECT 标志。如果没有设置过,默认的是非直接 I/O。 不过要注意,直接 I/O、非直接 I/O,本质上还是和文件系统交互。如果是在数据库等场景中,你还会看到,跳过文件系统读写磁盘的情况,也就是我们通常所说的裸 I/O。 第三,根据应用程序是否阻塞自身运行,可以把文件 I/O 分为阻塞 I/O 和非阻塞 I/O: 所谓阻塞 I/O,是指应用程序执行 I/O 操作后,如果没有获得响应,就会阻塞当前线程,自然就不能执行其他任务。 所谓非阻塞 I/O,是指应用程序执行 I/O 操作后,不会阻塞当前的线程,可以继续执行其他的任务,随后再通过轮询或者事件通知的形式,获取调用的结果。 比方说,访问管道或者网络套接字时,设置 O_NONBLOCK 标志,就表示用非阻塞方式访问;而如果不做任何设置,默认的就是阻塞访问。 第四,根据是否等待响应结果,可以把文件 I/O 分为同步和异步 I/O: 所谓同步 I/O,是指应用程序执行 I/O 操作后,要一直等到整个 I/O 完成后,才能获得 I/O 响应。 所谓异步 I/O,是指应用程序执行 I/O 操作后,不用等待完成和完成后的响应,而是继续执行就可以。等到这次 I/O 完成后,响应会用事件通知的方式,告诉应用程序。 举个例子,在操作文件时,如果你设置了 O_SYNC 或者 O_DSYNC 标志,就代表同步 I/O。如果设置了 O_DSYNC,就要等文件数据写入磁盘后,才能返回;而 O_SYNC,则是在 O_DSYNC 基础上,要求文件元数据也要写入磁盘后,才能返回。 再比如,在访问管道或者网络套接字时,设置了 O_ASYNC 选项后,相应的 I/O 就是异步 I/O。这样,内核会再通过 SIGIO 或者 SIGPOLL,来通知进程文件是否可读写。 你可能发现了,这里的好多概念也经常出现在网络编程中。比如非阻塞 I/O,通常会跟 select/poll 配合,用在网络套接字的 I/O 中。 1234查看索引节点信息$ df -i /dev/sda1Filesystem Inodes IUsed IFree IUse% Mounted on/dev/sda1 3870720 157460 3713260 5% / 索引节点的容量,(也就是 Inode 个数)是在格式化磁盘时设定好的,一般由格式化工具自动生成。当你发现索引节点空间不足,但磁盘空间充足时,很可能就是过多小文件导致的。 所以,一般来说,删除这些小文件,或者把它们移动到索引节点充足的其他磁盘中,就可以解决这个问题。 缓存在前面 Cache 案例中,我已经介绍过,可以用 free 或 vmstat,来观察页缓存的大小。复习一下,free 输出的 Cache,是页缓存和可回收 Slab 缓存的和,你可以从 /proc/meminfo ,直接得到它们的大小: 1234$ cat /proc/meminfo | grep -E \"SReclaimable|Cached\"Cached: 748316 kBSwapCached: 0 kBSReclaimable: 179508 kB 话说回来,文件系统中的目录项和索引节点缓存,又该如何观察呢? 实际上,内核使用 Slab 机制,管理目录项和索引节点的缓存。/proc/meminfo 只给出了 Slab 的整体大小,具体到每一种 Slab 缓存,还要查看 /proc/slabinfo 这个文件。 比如,运行下面的命令,你就可以得到,所有目录项和各种文件系统索引节点的缓存情况: 1234567891011$ cat /proc/slabinfo | grep -E '^#|dentry|inode'# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>xfs_inode 0 0 960 17 4 : tunables 0 0 0 : slabdata 0 0 0...ext4_inode_cache 32104 34590 1088 15 4 : tunables 0 0 0 : slabdata 2306 2306 0hugetlbfs_inode_cache 13 13 624 13 2 : tunables 0 0 0 : slabdata 1 1 0sock_inode_cache 1190 1242 704 23 4 : tunables 0 0 0 : slabdata 54 54 0shmem_inode_cache 1622 2139 712 23 4 : tunables 0 0 0 : slabdata 93 93 0proc_inode_cache 3560 4080 680 12 2 : tunables 0 0 0 : slabdata 340 340 0inode_cache 25172 25818 608 13 2 : tunables 0 0 0 : slabdata 1986 1986 0dentry 76050 121296 192 21 1 : tunables 0 0 0 : slabdata 5776 5776 0 这个界面中,dentry 行表示目录项缓存,inode_cache 行,表示 VFS 索引节点缓存,其余的则是各种文件系统的索引节点缓存。 /proc/slabinfo 的列比较多,具体含义你可以查询 man slabinfo。在实际性能分析中,我们更常使用 slabtop ,来找到占用内存最多的缓存类型。 比如,下面就是我运行 slabtop 得到的结果: 123456789101112131415# 按下c按照缓存大小排序,按下a按照活跃对象数排序$ slabtopActive / Total Objects (% used) : 277970 / 358914 (77.4%)Active / Total Slabs (% used) : 12414 / 12414 (100.0%)Active / Total Caches (% used) : 83 / 135 (61.5%)Active / Total Size (% used) : 57816.88K / 73307.70K (78.9%)Minimum / Average / Maximum Object : 0.01K / 0.20K / 22.88K OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME69804 23094 0% 0.19K 3324 21 13296K dentry16380 15854 0% 0.59K 1260 13 10080K inode_cache58260 55397 0% 0.13K 1942 30 7768K kernfs_node_cache 485 413 0% 5.69K 97 5 3104K task_struct 1472 1397 0% 2.00K 92 16 2944K kmalloc-2048 从这个结果你可以看到,在我的系统中,目录项和索引节点占用了最多的 Slab 缓存。不过它们占用的内存其实并不大,加起来也只有 23MB 左右。","tags":[]},{"title":"linux系统不可中断进程和僵尸进程","date":"2022-08-09T07:20:12.000Z","path":"2022/08/09/linux-02/","text":"进程状态当 iowait 升高时,进程很可能因为得不到硬件的响应,而长时间处于不可中断状态。从 ps 或者 top 命令的输出中,你可以发现它们都处于 D 状态,也就是不可中断状态(Uninterruptible Sleep)。既然说到了进程的状态,进程有哪些状态你还记得吗?我们先来回顾一下。 top 和 ps 是最常用的查看进程状态的工具,我们就从 top 的输出开始。下面是一个 top 命令输出的示例,S 列(也就是 Status 列)表示进程的状态。从这个示例里,你可以看到 R、D、Z、S、I 等几个状态,它们分别是什么意思呢? R 是 Running 或 Runnable 的缩写,表示进程在 CPU 的就绪队列中,正在运行或者正在等待运行。 D 是 Disk Sleep 的缩写,也就是不可中断状态睡眠(Uninterruptible Sleep),一般表示进程正在跟硬件交互,并且交互过程不允许被其他进程或中断打断。 Z 是 Zombie 的缩写,如果你玩过“植物大战僵尸”这款游戏,应该知道它的意思。它表示僵尸进程,也就是进程实际上已经结束了,但是父进程还没有回收它的资源(比如进程的描述符、PID 等)。 S 是 Interruptible Sleep 的缩写,也就是可中断状态睡眠,表示进程因为等待某个事件而被系统挂起。当进程等待的事件发生时,它会被唤醒并进入 R 状态。 I 是 Idle 的缩写,也就是空闲状态,用在不可中断睡眠的内核线程上。前面说了,硬件交互导致的不可中断进程用 D 表示,但对某些内核线程来说,它们有可能实际上并没有任何负载,用 Idle 正是为了区分这种情况。要注意,D 状态的进程会导致平均负载升高, I 状态的进程却不会。 当然了,上面的示例并没有包括进程的所有状态。除了以上 5 个状态,进程还包括下面这 2 个状态。 第一个是 T 或者 t,也就是 Stopped 或 Traced 的缩写,表示进程处于暂停或者跟踪状态。 向一个进程发送 SIGSTOP 信号,它就会因响应这个信号变成暂停状态(Stopped);再向它发送 SIGCONT 信号,进程又会恢复运行(如果进程是终端里直接启动的,则需要你用 fg 命令,恢复到前台运行)。 而当你用调试器(如 gdb)调试一个进程时,在使用断点中断进程后,进程就会变成跟踪状态,这其实也是一种特殊的暂停状态,只不过你可以用调试器来跟踪并按需要控制进程的运行。 另一个是 X,也就是 Dead 的缩写,表示进程已经消亡,所以你不会在 top 或者 ps 命令中看到它。 但如果系统或硬件发生了故障,进程可能会在不可中断状态保持很久,甚至导致系统中出现大量不可中断进程。这时,你就得注意下,系统是不是出现了 I/O 等性能问题。 再看僵尸进程,这是多进程应用很容易碰到的问题。正常情况下,当一个进程创建了子进程后,它应该通过系统调用 wait() 或者 waitpid() 等待子进程结束,回收子进程的资源;而子进程在结束时,会向它的父进程发送 SIGCHLD 信号,所以,父进程还可以注册 SIGCHLD 信号的处理函数,异步回收资源。 如果父进程没这么做,或是子进程执行太快,父进程还没来得及处理子进程状态,子进程就已经提前退出,那这时的子进程就会变成僵尸进程。换句话说,父亲应该一直对儿子负责,善始善终,如果不作为或者跟不上,都会导致“问题少年”的出现。 通常,僵尸进程持续的时间都比较短,在父进程回收它的资源后就会消亡;或者在父进程退出后,由 init 进程回收后也会消亡。 一旦父进程没有处理子进程的终止,还一直保持运行状态,那么子进程就会一直处于僵尸状态。大量的僵尸进程会用尽 PID 进程号,导致新进程不能创建,所以这种情况一定要避免。 第一点,iowait 太高了,导致系统的平均负载升高,甚至达到了系统 CPU 的个数。 第二点,僵尸进程在不断增多,说明有程序没能正确清理子进程的资源。 不可中断状态,表示进程正在跟硬件交互,为了保护进程数据和硬件的一致性,系统不允许其他进程或中断打断这个进程。进程长时间处于不可中断状态,通常表示系统有 I/O 性能问题。 僵尸进程表示进程已经退出,但它的父进程还没有回收子进程占用的资源。短暂的僵尸状态我们通常不必理会,但进程长时间处于僵尸状态,就应该注意了,可能有应用程序没有正常处理子进程的退出。 iowait 分析推荐安装的 dstat ,它的好处是,可以同时查看 CPU 和 I/O 这两种资源的使用情况,便于对比分析。 12345678910111213141516# 间隔1秒输出10组数据$ dstat 1 10You did not select any stats, using -cdngy by default.--total-cpu-usage-- -dsk/total- -net/total- ---paging-- ---system--usr sys idl wai stl| read writ| recv send| in out | int csw 0 0 96 4 0|1219k 408k| 0 0 | 0 0 | 42 885 0 0 2 98 0| 34M 0 | 198B 790B| 0 0 | 42 138 0 0 0 100 0| 34M 0 | 66B 342B| 0 0 | 42 135 0 0 84 16 0|5633k 0 | 66B 342B| 0 0 | 52 177 0 3 39 58 0| 22M 0 | 66B 342B| 0 0 | 43 144 0 0 0 100 0| 34M 0 | 200B 450B| 0 0 | 46 147 0 0 2 98 0| 34M 0 | 66B 342B| 0 0 | 45 134 0 0 0 100 0| 34M 0 | 66B 342B| 0 0 | 39 131 0 0 83 17 0|5633k 0 | 66B 342B| 0 0 | 46 168 0 3 39 59 0| 22M 0 | 66B 342B| 0 0 | 37 134 从 dstat 的输出,我们可以看到,每当 iowait 升高(wai)时,磁盘的读请求(read)都会很大。这说明 iowait 的升高跟磁盘的读请求有关,很可能就是磁盘读导致的。 1234567# -d 展示 I/O 统计数据,-p 指定进程号,间隔 1 秒输出 3 组数据$ pidstat -d -p 4344 1 306:38:50 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command06:38:51 0 4344 0.00 0.00 0.00 0 app06:38:52 0 4344 0.00 0.00 0.00 0 app06:38:53 0 4344 0.00 0.00 0.00 0 app 在这个输出中, kB_rd 表示每秒读的 KB 数, kB_wr 表示每秒写的 KB 数,iodelay 表示 I/O 的延迟(单位是时钟周期)。它们都是 0,那就表示此时没有任何的读写,说明问题不是 4344 进程导致的。 看所有的进程的情况: 123456789# 间隔 1 秒输出多组数据 (这里是 20 组)$ pidstat -d 1 20perf record -gperf report 直接读写磁盘,对 I/O 敏感型应用(比如数据库系统)是很友好的,因为你可以在应用中,直接控制磁盘的读写。但在大部分情况下,我们最好还是通过系统缓存来优化磁盘 I/O,换句话说,删除 O_DIRECT 这个选项就是了。 僵尸进程接下来,我们就来处理僵尸进程的问题。既然僵尸进程是因为父进程没有回收子进程的资源而出现的,那么,要解决掉它们,就要找到它们的根儿,也就是找出父进程,然后在父进程里解决。 父进程的找法我们前面讲过,最简单的就是运行 pstree 命令: 1234567891011# -a 表示输出命令行选项# p表PID# s表示指定进程的父进程$ pstree -aps 3084systemd,1 └─dockerd,15006 -H fd:// └─docker-containe,15024 --config /var/run/docker/containerd/containerd.toml └─docker-containe,3991 -namespace moby -workdir... └─app,4009 └─(app,3084) 小结今天我用一个多进程的案例,带你分析系统等待 I/O 的 CPU 使用率(也就是 iowait%)升高的情况。 虽然这个案例是磁盘 I/O 导致了 iowait 升高,不过, iowait 高不一定代表 I/O 有性能瓶颈。当系统中只有 I/O 类型的进程在运行时,iowait 也会很高,但实际上,磁盘的读写远没有达到性能瓶颈的程度。 因此,碰到 iowait 升高时,需要先用 dstat、pidstat 等工具,确认是不是磁盘 I/O 的问题,然后再找是哪些进程导致了 I/O。 等待 I/O 的进程一般是不可中断状态,所以用 ps 命令找到的 D 状态(即不可中断状态)的进程,多为可疑进程。但这个案例中,在 I/O 操作后,进程又变成了僵尸进程,所以不能用 strace 直接分析这个进程的系统调用。 这种情况下,我们用了 perf 工具,来分析系统的 CPU 时钟事件,最终发现是直接 I/O 导致的问题。这时,再检查源码中对应位置的问题,就很轻松了。 而僵尸进程的问题相对容易排查,使用 pstree 找出父进程后,去查看父进程的代码,检查 wait() / waitpid() 的调用,或是 SIGCHLD 信号处理函数的注册就行了。","tags":[]},{"title":"Linux优化","date":"2022-08-09T07:19:12.000Z","path":"2022/08/09/linux-01/","text":"java系统优化参数查看jps jstat -gcutil pid 100(滚动时间) jstack -l pid #堆信息jmap -histo:live pid jmap -heap pid jcmd pid help jcmd pid VM.version jcmd pid GC.run Arthas jvm调优工具 Linux系统资源1234567891011121314151617181920ps aux --sort -%mem | head -n 7ps aux --sort -%cpu | head -n 7df -hdu -h查看每个目录大小sudo du -sh *统计目录多少du -sm * | sort -nfree -hfree -mfree -g 进程实时情况:1watch -n 1 free -h 堆栈信息:12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061sudo vi gstack#!/bin/shif test $# -ne 1; then echo \"Usage: `basename $0 .sh` <process-id>\" 1>&2 exit 1fiif test ! -r /proc/$1; then echo \"Process $1 not found.\" 1>&2 exit 1fi# GDB doesn't allow \"thread apply all bt\" when the process isn't# threaded; need to peek at the process to determine if that or the# simpler \"bt\" should be used.backtrace=\"bt\"if test -d /proc/$1/task ; then # Newer kernel; has a task/ directory. if test `/bin/ls /proc/$1/task | /usr/bin/wc -l` -gt 1 2>/dev/null ; then backtrace=\"thread apply all bt\" fielif test -f /proc/$1/maps ; then # Older kernel; go by it loading libpthread. if /bin/grep -e libpthread /proc/$1/maps > /dev/null 2>&1 ; then backtrace=\"thread apply all bt\" fifiGDB=${GDB:-/usr/bin/gdb}if $GDB -nx --quiet --batch --readnever > /dev/null 2>&1; then readnever=--readneverelse readnever=fi# Run GDB, strip out unwanted noise.$GDB --quiet $readnever -nx /proc/$1/exe $1 <<EOF 2>&1 |set width 0set height 0set pagination no$backtraceEOF/bin/sed -n \\ -e 's/^\\((gdb) \\)*//' \\ -e '/^#/p' \\ -e '/^Thread/p'#endsudo chmod 777 gstacksudo cp gstack /usr/bin/堆栈信息:sudo gstack pid线程:top -Hp pid 内核1https://www.kernel.org/doc/html/v4.10/dev-tools/kmemleak.html 查看内存占用最多的进程:1ps -aux | sort -k4nr | head -n 5 内存使用1234sudo sucat /proc/meminfocat /proc/zoneinfo 树状以及块存储结构123pstree -hsudo dumpe2fs /dev/sda1dumpe2fs /dev/sda1 | grep -i \"block size\" 句柄查看1234567891011121314151617181920212223242526sudo su总文件句柄数cat /proc/sys/fs/file-max修改总:sysctl -w fs.file-max=(成倍增加)当前文件句柄数cat /proc/sys/fs/file-nrpmap ${pid}ulimit -nulimit -HSn 2048lsof -n|awk '{print $2}'|sort|uniq -c|sort -nr|morelsof |grep -i deletedsudo lsof -p 9268cd /proc/9268/fdls | wc -lls -al /proc/9689/fd/ | wc -l 更新本地软件库123更新本地软件列表sudo apt updatesudo apt search 名称 查看IO、CPU利用率12top -Hp pidvmstat 2 10000 12345虽然同是写数据,写磁盘跟写文件的现象还是不同的。写磁盘时(也就是 bo 大于 0 时),Buffer 和 Cache 都在增长,但显然 Buffer 的增长快得多。这说明,写磁盘用到了大量的Buffer,这跟我们在文档中查到的定义是一样的。对比两个案例,我们发现,写文件时会用到 Cache 缓存数据,而写磁盘则会用到 Buffer来缓存数据。所以,回到刚刚的问题,虽然文档上只提到,Cache是文件读的缓存,但实际上,Cache 也会缓存写文件时的数据。 swapSwap分区(也称交换分区)是硬盘上的一个区域,被指定为操作系统可以临时存储数据的地方,这些数据不能再保存在RAM中。 基本上,这使您能够增加服务器在工作“内存”中保留的信息量,但有一些注意事项,主要是当RAM中没有足够的空间容纳正在使用的应用程序数据时,将使用硬盘驱动器上的交换空间。 写入磁盘的信息将比保存在RAM中的信息慢得多,但是操作系统更愿意将应用程序数据保存在内存中,并使用交换旧数据。 总的来说,当系统的RAM耗尽时,将交换空间作为回落空间可能是一个很好的安全网,可防止非SSD存储系统出现内存不足的情况。 内存不够用?在Linux上使用swapfile配置交换空间 查看系统是否有交换分区: 1sudo swapon --show 临时修改方法如下: 1234sudo fallocate -l 4G /swapfilesudo chmod 600 /swapfilesudo mkswap -f /swapfilesudo swapon /swapfile 经过测试,OpenSuSE系统要使用以下命令才能成功创建swapfile 1sudo dd if=/dev/zero of=/swapfile count=4096 bs=1MiB 使用以下命令查看是否正确创建。 1ls -lh /swapfile 结果应该类似下面这样: 1-rw-r--r-- 1 root root 4.0G Apr 26 17:04 /swapfile 修改swapfile权限 1sudo chmod 600 /swapfile 查看效果 1ls -lh /swapfile 结果应该类似下面这样: 1-rw------- 1 root root 8.0G Apr 26 17:04 /swapfile 激活交换空间 12sudo mkswap /swapfilesudo swapon /swapfile 之后使用以下命令查看使用成功开启交换空间: 1sudo swapon --show 结果类似下面这样: 12NAME TYPE SIZE USED PRIO/swapfile file 8G 0B -1 添加到fstab 这样每次开机系统就会自动吧swapfile挂载为交换空间。 首先请自行备份fstab文件。 然后把以下配置添加到fstab文件末尾。 1/swapfile none swap sw 0 0 或者直接使用以下命令: 12345678910sudo cp /etc/fstab /etc/fstab.bakecho '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab#关闭swapfile区sudo swapoff /swapfilesudo rm /swapfile 缓存释放如果你使用过 drop_cache 来释放 inode 的话,应该会清楚它有几个控制选项,我们可以通过写入不同的数值来释放不同类型的 cache(用户数据 Page Cache,内核数据 Slab,或者二者都释放),这些选项你可以去看Kernel Documentation(https://www.kernel.org/doc/Documentation/sysctl/vm.txt) 的描述。 于是这样就引入了一个容易被我们忽略的问题:当我们执行 echo 2 来 drop slab 的时候,它也会把 Page Cache 给 drop 掉,很多运维人员都会忽视掉这一点。 在系统内存紧张的时候,运维人员或者开发人员会想要通过 drop_caches 的方式来释放一些内存,但是由于他们清楚 Page Cache 被释放掉会影响业务性能,所以就期望只去 drop slab 而不去 drop pagecache。于是很多人这个时候就运行 echo 2 > /proc/sys/vm/drop_caches,但是结果却出乎了他们的意料:Page Cache 也被释放掉了,业务性能产生了明显的下降。 由于 drop_caches 是一种内存事件,内核会在 /proc/vmstat 中来记录这一事件,所以我们可以通过 /proc/vmstat 来判断是否有执行过 drop_caches。 123$ grep drop /proc/vmstatdrop_pagecache 3drop_slab 2 如上所示,它们分别意味着 pagecache 被 drop 了 3 次(通过 echo 1 或者 echo 3),slab 被 drop 了 2 次(通过 echo 2 或者 echo 3)。如果这两个值在问题发生前后没有变化,那就可以排除是有人执行了 drop_caches;否则可以认为是因为 drop_caches 引起的 Page Cache 被回收。 12345678910111213141516171819202122#清理文件页、目录项、Inodes等各种缓存echo 3 > /proc/sys/vm/drop_caches#然后运行dd命令随机读取设备,向磁盘分区/dev/sdb1写入2G数据dd if=/dev/urandom of=/dev/sdb1 bs=1M count=2048sudo suvi /root/cache.sh#! /bin/bash#v1.0syncecho 3 > /proc/sys/vm/drop_cachesswapoff -a && swapon -acrontab -e*/2 * * * * /root/cache.sh 内存泄露排查1234567891011121314151617181920212223占用线程的内存文件cat /proc/31108/smapssudo sucat /proc/meminfo其中 vmalloc 申请的内存会体现在 VmallocUsed 这一项中,即已使用的 Vmalloc 区大小;而 kmalloc 申请的内存则是体现在 Slab 这一项中,它又分为两部分,其中 SReclaimable 是指在内存紧张的时候可以被回收的内存,而 SUnreclaim 则是不可以被回收只能主动释放的内存。如果 /proc/meminfo 中内核内存(比如 VmallocUsed 和 SUnreclaim)太大,那很有可能发生了内核内存泄漏;另外,你也可以周期性地观察 VmallocUsed 和 SUnreclaim 的变化,如果它们持续增长而不下降,也可能是发生了内核内存泄漏。这也可以通过 /proc 来查看,所以再次强调一遍,当你不清楚该如何去分析时,你可以试着去查看 /proc 目录下的文件。以上面的程序为例,安装 kmem_test 这个内核模块后,我们可以通过 /proc/vmallocinfo 来看到该模块的内存使用情况:$ cat /proc/vmallocinfo | grep appNamevmstat 2 10000中看free的变化速度(迅速下降,但buffer、cache没发生变化,存在泄露)strace -t -f -p 31108 -o 31108.stracecat 31108.strace | grep 10489856 内存回收(1)代码区——–主要存储程序代码指令,define定义的常量。 (2)全局数据区——主要存储全局变量(常量),静态变量(常量),常量字符串。 (3)栈区——–主要存储局部变量,栈区上的内容只在函数范围内存在,当函数运行结束,这些内容也会自动被销毁。其特点是效率高,但内存大小有限。 (4)堆区——–由malloc,calloc分配的内存区域,其生命周期由free决定。堆的内存大小是由程序员分配的,理论上可以占据系统中的所有内存。 当发生了内存泄漏时,或者运行了大内存的应用程序,导致系统的内存资源紧张时,系统又会如何应对呢? 这其实会导致两种可能结果,内存回收和OOM杀死进程。我们先来看后一个可能结果,内存资源紧张导致的 OOM(Out OfMemory),相对容易理解,指的是系统杀死占用大量内存的进程,释放这些内存,再分配给其他更需要的进程。 这一点我们前面详细讲过,这里就不再重复了。接下来再看第一个可能的结果,内存回收,也就是系统释放掉可以回收的内存,比如我前面讲过的缓存和缓冲区,就属于可回收内存。它们在内存管理中,通常被叫做文件页(File-backed Page)。 大部分文件页,都可以直接回收,以后有需要时,再从磁盘重新读取就可以了。而那些被应用程序修改过,并且暂时还没写入磁盘的数据(也就是脏页),就得先写入磁盘,然后才能进行内存释放。 这些脏页,一般可以通过两种方式写入磁盘。 可以在应用程序中,通过系统调用 fsync ,把脏页同步到磁盘中; 也可以交给系统,由内核线程 pdflush 负责这些脏页的刷新。(内核使用pdflush线程刷新脏页到磁盘,pdflush线程个数在2和8之间,可以通过/proc/sys/vm/nr_pdflush_threads文件直接查看,具体策略机制参看源码函数__pdflush。) kbdirty 就是系统中的脏页大小,它同样也是对 /proc/vmstat 中 nr_dirty 的解析。你可以通过调小如下设置来将系统脏页个数控制在一个合理范围: 12345vm.dirty_background_bytes=0vm.dirty_background_ratio=10vm.dirty_bytes=0vm.dirty_expire_centisecs=3000vm.dirty_ratio=20 调整这些配置项有利有弊,调大这些值会导致脏页的积压,但是同时也可能减少了 I/O 的次数,从而提升单次刷盘的效率;调小这些值可以减少脏页的积压,但是同时也增加了 I/O 的次数,降低了 I/O 的效率。 至于这些值调整大多少比较合适,也是因系统和业务的不同而异,我的建议也是一边调整一边观察,将这些值调整到业务可以容忍的程度就可以了,即在调整后需要观察业务的服务质量 (SLA),要确保 SLA 在可接受范围内。调整的效果你可以通过 /proc/vmstat 来查看: 123grep \"nr_dirty_\" /proc/vmstatnr_dirty_threshold 366998nr_dirty_background_threshold 183275 你可以观察一下调整前后这两项的变化。这里我要给你一个避免踩坑的提示,解决该方案中的设置项如果设置不妥会触发一个内核 Bug,这是我在 2017 年进行性能调优时发现的一个内核 Bug,我给社区提交了一个 patch 将它 fix 掉了,具体的 commit 见 writeback: schedule periodic writeback with sysctl , commit log 清晰地描述了该问题,我建议你有时间看一看。 Page Cache是怎样产生和释放的Page Cache 的产生有两种不同的方式: Buffered I/O(标准 I/O); Memory-Mapped I/O(存储映射 I/O)。 标准 I/O 是写的 (write(2)) 用户缓冲区 (Userpace Page 对应的内存),然后再将用户缓冲区里的数据拷贝到内核缓冲区 (Pagecache Page 对应的内存);如果是读的 (read(2)) 话则是先从内核缓冲区拷贝到用户缓冲区,再从用户缓冲区读数据,也就是 buffer 和文件内容不存在任何映射关系。 对于存储映射 I/O 而言,则是直接将 Pagecache Page 给映射到用户地址空间,用户直接读写 Pagecache Page 中内容。 1234cat /proc/vmstat | egrep \"dirty|writeback\"nr_dirty 40nr_writeback 2 如上所示,nr_dirty 表示当前系统中积压了多少脏页,nr_writeback 则表示有多少脏页正在回写到磁盘中,他们两个的单位都是 Page(4KB)。 释放free 命令中的 buff/cache 中的这些就是“活着”的 Page Cache,那它们什么时候会“死亡”(被回收)呢?我们来看一张图: 应用在申请内存的时候,即使没有 free 内存,只要还有足够可回收的 Page Cache,就可以通过回收 Page Cache 的方式来申请到内存,回收的方式主要是两种:直接回收和后台回收。 那它是具体怎么回收的呢?你要怎么观察呢?其实在我看来,观察 Page Cache 直接回收和后台回收最简单方便的方式是使用 sar: 123456789101112$ sar -r 1$ sar -B 102:14:01 PM pgpgin/s pgpgout/s fault/s majflt/s pgfree/s pgscank/s pgscand/s pgsteal/s %vmeff02:14:01 PM 0.14 841.53 106745.40 0.00 41936.13 0.00 0.00 0.00 0.0002:15:01 PM 5.84 840.97 86713.56 0.00 43612.15 717.81 0.00 717.66 99.9802:16:01 PM 95.02 816.53 100707.84 0.13 46525.81 3557.90 0.00 3556.14 99.9502:17:01 PM 10.56 901.38 122726.31 0.27 54936.13 8791.40 0.00 8790.17 99.9902:18:01 PM 108.14 306.69 96519.75 1.15 67410.50 14315.98 31.48 14319.38 99.8002:19:01 PM 5.97 489.67 88026.03 0.18 48526.07 1061.53 0.00 1061.42 99.99 借助上面这些指标,你可以更加明确地观察内存回收行为,下面是这些指标的具体含义: pgscank/s : kswapd(后台回收线程) 每秒扫描的 page 个数。 pgscand/s: Application 在内存申请过程中每秒直接扫描的 page 个数。 pgsteal/s: 扫描的 page 中每秒被回收的个数。 %vmeff: pgsteal/(pgscank+pgscand), 回收效率,越接近 100 说明系统越安全,越接近 0 说明系统内存压力越大。 **进程运行所需要的内存类型有很多种,总的来说,这些内存类型可以从是不是文件映射,以及是不是私有内存这两个不同的维度来做区分,也就是可以划分为四类内存。** 私有匿名内存。进程的堆、栈,以及 mmap(MAP_ANON | MAP_PRIVATE) 这种方式申请的内存都属于这种类型的内存。其中栈是由操作系统来进行管理的,应用程序无需关注它的申请和释放;堆和私有匿名映射则是由应用程序(程序员)来进行管理的,它们的申请和释放都是由应用程序来负责的,所以它们是容易产生内存泄漏的地方。 共享匿名内存。进程通过 mmap(MAP_ANON | MAP_SHARED) 这种方式来申请的内存,比如说 tmpfs 和 shm。这个类型的内存也是由应用程序来进行管理的,所以也可能会发生内存泄漏。 私有文件映射。进程通过 mmap(MAP_FILE | MAP_PRIVATE) 这种方式来申请的内存,比如进程将共享库(Shared libraries)和可执行文件的代码段(Text Segment)映射到自己的地址空间就是通过这种方式。对于共享库和可执行文件的代码段的映射,这是通过操作系统来进行管理的,应用程序无需关注它们的申请和释放。而应用程序直接通过 mmap(MAP_FILE | MAP_PRIVATE) 来申请的内存则是需要应用程序自己来进行管理,这也是可能会发生内存泄漏的地方。 共享文件映射。进程通过 mmap(MAP_FILE | MAP_SHARED) 这种方式来申请的内存,我们在上一个模块课程中讲到的 File Page Cache 就属于这类内存。这部分内存也需要应用程序来申请和释放,所以也存在内存泄漏的可能性。 总结 进程直接读写的都是虚拟地址,虚拟地址最终会通过 Paging(分页)来转换为物理内存的地址,Paging 这个过程是由内核来完成的。 进程的内存类型可以从 anon(匿名)与 file(文件)、private(私有)与 shared(共享)这四项来区分为 4 种不同的类型,进程相关的所有内存都是这几种方式的不同组合。 查看进程内存时,可以先使用 top 来看系统中各个进程的内存使用概况,再使用 pmap 去观察某个进程的内存细节。 直接内存回收是指在进程上下文同步进行内存回收,那么它具体是怎么引起 load 飙高的呢?因为直接内存回收是在进程申请内存的过程中同步进行的回收,而这个回收过程可能会消耗很多时间,进而导致进程的后续行为都被迫等待,这样就会造成很长时间的延迟,以及系统的 CPU 利用率会升高,最终引起 load 飙高。 那么,针对直接内存回收引起 load 飙高或者业务 RT 抖动的问题,一个解决方案就是及早地触发后台回收来避免应用程序进行直接内存回收,那具体要怎么做呢?那么,我们可以增大 min_free_kbytes 这个配置选项来及早地触发后台回收,该选项最终控制的是内存回收水位 系统中脏页过多引起 load 飙高那如何解决这类问题呢?一个比较省事的解决方案是控制好系统中积压的脏页数据。很多人知道需要控制脏页,但是往往并不清楚如何来控制好这个度,脏页控制的少了可能会影响系统整体的效率,脏页控制的多了还是会触发问题,所以我们接下来看下如何来衡量好这个“度”。 kbdirty 就是系统中的脏页大小,它同样也是对 /proc/vmstat 中 nr_dirty 的解析。你可以通过调小如下设置来将系统脏页个数控制在一个合理范围: 12345vm.dirty_background_bytes=0vm.dirty_background_ratio=10vm.dirty_bytes=0vm.dirty_expire_centisecs=3000vm.dirty_ratio=20 调整这些配置项有利有弊,调大这些值会导致脏页的积压,但是同时也可能减少了 I/O 的次数,从而提升单次刷盘的效率;调小这些值可以减少脏页的积压,但是同时也增加了 I/O 的次数,降低了 I/O 的效率。 至于这些值调整大多少比较合适,也是因系统和业务的不同而异,我的建议也是一边调整一边观察,将这些值调整到业务可以容忍的程度就可以了,即在调整后需要观察业务的服务质量 (SLA),要确保 SLA 在可接受范围内。调整的效果你可以通过 /proc/vmstat 来查看: 123grep \"nr_dirty_\" /proc/vmstatnr_dirty_threshold 366998nr_dirty_background_threshold 183275 你可以观察一下调整前后这两项的变化。这里我要给你一个避免踩坑的提示,解决该方案中的设置项如果设置不妥会触发一个内核 Bug,这是我在 2017 年进行性能调优时发现的一个内核 Bug,我给社区提交了一个 patch 将它 fix 掉了,具体的 commit 见 writeback: schedule periodic writeback with sysctl , commit log 清晰地描述了该问题,我建议你有时间看一看。 系统 NUMA 策略配置不当引起的 load 飙高除了我前面提到的这两种引起系统 load 飙高或者业务延迟抖动的场景之外,还有另外一种场景也会引起 load 飙高,那就是系统 NUMA 策略配置不当引起的 load 飙高。 比如说,我们在生产环境上就曾经遇到这样的问题:系统中还有一半左右的 free 内存,但还是频频触发 direct reclaim,导致业务抖动得比较厉害。后来经过排查发现是由于设置了 zone_reclaim_mode,这是 NUMA 策略的一种。 设置 zone_reclaim_mode 的目的是为了增加业务的 NUMA 亲和性,但是在实际生产环境中很少会有对 NUMA 特别敏感的业务,这也是为什么内核将该配置从默认配置 1 修改为了默认配置 0: mm: disable zone_reclaim_mode by default ,配置为 0 之后,就避免了在其他 node 有空闲内存时,不去使用这些空闲内存而是去回收当前 node 的 Page Cache,也就是说,通过减少内存回收发生的可能性从而避免它引发的业务延迟。 那么如何来有效地衡量业务延迟问题是否由 zone reclaim 引起的呢?它引起的延迟究竟有多大呢?这个衡量和观察方法也是我贡献给 Linux Kernel 的:mm/vmscan: add tracepoints for node reclaim ,大致的思路就是利用 linux 的 tracepoint 来做这种量化分析,这是性能开销相对较小的一个方案。 推荐将 zone_reclaim_mode 配置为 0。vm.zone_reclaim_mode = 0因为相比内存回收的危害而言,NUMA 带来的性能提升几乎可以忽略,所以配置为 0,利远大于弊。 好了,对于 Page Cache 管理不当引起的系统 load 飙高和业务时延抖动问题,我们就分析到这里,希望通过这篇的学习,在下次你遇到直接内存回收引起的 load 飙高问题时不再束手无策。 总的来说,这些问题都是 Page Cache 难以释放而产生的问题,那你是否想过,是不是 Page Cache 很容易释放就不会产生问题了?这个答案可能会让你有些意料不到:Page Cache 容易释放也有容易释放的问题。这到底是怎么回事呢,我们下节课来分析下这方面的案例。 内核机制引起 Page Cache 被回收而产生的业务性能下降我简单来解释一下这个图。Reclaimer 是指回收者,它可以是内核线程(包括 kswapd)也可以是用户线程。回收的时候,它会依次来扫描 pagecache page 和 slab page 中有哪些可以被回收的,如果有的话就会尝试去回收,如果没有的话就跳过。在扫描可回收 page 的过程中回收者一开始扫描的较少,然后逐渐增加扫描比例直至全部都被扫描完。这就是内存回收的大致过程。 接下来我所要讲述的案例就发生在“relcaim slab”中,我们从前一个案例已然知道,如果 inode 被回收的话,那么它对应的 Page Cache 也都会被回收掉,所以如果业务进程读取的文件对应的 inode 被回收了,那么该文件所有的 Page Cache 都会被释放掉,这也是容易引起性能问题的地方。 那这个行为是否有办法观察?这同样也是可以通过 /proc/vmstat 来观察的,/proc/vmstat 简直无所不能(这也是为什么我会在之前说内核开发者更习惯去观察 /proc/vmstat)。 123$ grep inodesteal /proc/vmstatpginodesteal 114341kswapd_inodesteal 1291853 这个行为对应的事件是 inodesteal,就是上面这两个事件,其中 kswapd_inodesteal 是指在 kswapd 回收的过程中,因为回收 inode 而释放的 pagecache page 个数;pginodesteal 是指 kswapd 之外其他线程在回收过程中,因为回收 inode 而释放的 pagecache page 个数。所以在你发现业务的 Page Cache 被释放掉后,你可以通过观察来发现是否因为该事件导致的。 如何避免 Page Cache 被回收而引起的性能问题?我们在分析一些问题时,往往都会想这个问题是我的模块有问题呢,还是别人的模块有问题。也就是说,是需要修改我的模块来解决问题还是需要修改其他模块来解决问题。与此类似,避免 Page Cache 里相对比较重要的数据被回收掉的思路也是有两种: 从应用代码层面来优化; 从系统层面来调整。 从应用程序代码层面来解决是相对比较彻底的方案,因为应用更清楚哪些 Page Cache 是重要的,哪些是不重要的,所以就可以明确地来对读写文件过程中产生的 Page Cache 区别对待。比如说,对于重要的数据,可以通过 mlock(2) 来保护它,防止被回收以及被 drop;对于不重要的数据(比如日志),那可以通过 madvise(2) 告诉内核来立即释放这些 Page Cache。 在有些情况下,对应用程序而言,修改源码是件比较麻烦的事,如果可以不修改源码来达到目的那就最好不过了。Linux 内核同样实现了这种不改应用程序的源码而从系统层面调整来保护重要数据的机制,这个机制就是 memory cgroup protection。 它大致的思路是,将需要保护的应用程序使用 memory cgroup 来保护起来,这样该应用程序读写文件过程中所产生的 Page Cache 就会被保护起来不被回收或者最后被回收。memory cgroup protection 大致的原理如下图所示: 如上图所示,memory cgroup 提供了几个内存水位控制线 memory.{min, low, high, max} 。 memory.max这是指 memory cgroup 内的进程最多能够分配的内存,如果不设置的话,就默认不做内存大小的限制。 memory.high如果设置了这一项,当 memory cgroup 内进程的内存使用量超过了该值后就会立即被回收掉,所以这一项的目的是为了尽快的回收掉不活跃的 Page Cache。 memory.low这一项是用来保护重要数据的,当 memory cgroup 内进程的内存使用量低于了该值后,在内存紧张触发回收后就会先去回收不属于该 memory cgroup 的 Page Cache,等到其他的 Page Cache 都被回收掉后再来回收这些 Page Cache。 memory.min这一项同样是用来保护重要数据的,只不过与 memoy.low 有所不同的是,当 memory cgroup 内进程的内存使用量低于该值后,即使其他不在该 memory cgroup 内的 Page Cache 都被回收完了也不会去回收这些 Page Cache,可以理解为这是用来保护最高优先级的数据的。 那么,如果你想要保护你的 Page Cache 不被回收,你就可以考虑将你的业务进程放在一个 memory cgroup 中,然后设置 memory.{min,low} 来进行保护;与之相反,如果你想要尽快释放你的 Page Cache,那你可以考虑设置 memory.high 来及时的释放掉不活跃的 Page Cache。 除了缓存和缓冲区,通过内存映射获取的文件映射页,也是一种常见的文件页。它也可以被释放掉,下次再访问的时候,从文件重新读取。 除了文件页外,还有没有其他的内存可以回收呢?比如,应用程序动态分配的堆内存,也就是我们在内存管理中说到的匿名页(Anonymous Page),是不是也可以回收呢? 我想,你肯定会说,它们很可能还要再次被访问啊,当然不能直接回收了。非常正确,这些内存自然不能直接释放。 但是,如果这些内存在分配后很少被访问,似乎也是一种资源浪费。是不是可以把它们暂时先存在磁盘里,释放内存给其他更需要的进程? 其实,这正是 Linux 的Swap机制。Swap把这些不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了。 在前几节的案例中,我们已经分别学过缓存和 OOM 的原理和分析。那 Swap 又是怎么工作的呢?因为内容比较多,接下来,我将用两节课的内容,带你探索 Swap 的工作原理,以及 Swap 升高后的分析方法。 今天我们先来看看,Swap 究竟是怎么工作的。 Swap 原理Swap 说白了就是把一块磁盘空间或者一个本地文件(以下讲解以磁盘为例),当成内存来使用。它包括换出和换入两个过程。 所谓换出,就是把进程暂时不用的内存(swap)数据存储到磁盘中,并释放这些数据占用的内存。 而换入,则是在进程再次访问这些内存的时候,把它们从磁盘读到内存(swap)中来。 所以你看,Swap 其实是把系统的可用内存变大了。这样,即使服务器的内存不足,也可以运行大内存的应用程序。 还记得我最早学习 Linux操作系统时,内存实在太贵了,一个普通学生根本就用不起大的内存,那会儿我就是开启了Swap来运行Linux桌面。当然,现在的内存便宜多了,服务器一般也会配置很大的内存,那是不是说Swap就没有用武之地了呢? 当然不是。事实上,内存再大,对应用程序来说,也有不够用的时候。 一个很典型的场景就是,即使内存不足时,有些应用程序也并不想被 OOM 杀死,而是希望能缓一段时间,等待人工介入,或者等系统自动释放其他进程的内存,再分配给它。 除此之外,我们常见的笔记本电脑的休眠和快速开机的功能,也基于 Swap 。休眠时,把系统的内存存入磁盘,这样等到再次开机时,只要从磁盘中加载内存就可以。这样就省去了很多应用程序的初始化过程,加快了开机速度。 话说回来,既然 Swap 是为了回收内存,那么Linux到底在什么时候需要回收内存呢?前面一直在说内存资源紧张,又该怎么来衡量内存是不是紧张呢? 一个最容易想到的场景就是,有新的大块内存分配请求,但是剩余内存不足。这个时候系统就需要回收一部分内存(比如前面提到的缓存),进而尽可能地满足新内存请求。这个过程通常被称为直接内存回收。 除了直接内存回收,还有一个专门的内核线程用来定期回收内存,也就是 kswapd0。为了衡量内存的使用情况,kswapd0 定义了三个内存阈值(watermark,也称为水位),分别是 页最小阈值(pages_min)、页低阈值(pages_low)和页高阈值(pages_high)。剩余内存,则使用 pages_free 表示。 kswapd0 定期扫描内存的使用情况,并根据剩余内存落在这三个阈值的空间位置,进行内存的回收操作。 剩余内存小于页最小阈值,说明进程可用内存都耗尽了,只有内核才可以分配内存。 剩余内存落在页最小阈值和页低阈值中间,说明内存压力比较大,剩余内存不多了。这时 kswapd0 会执行内存回收,直到剩余内存大于高阈值为止。 剩余内存落在页低阈值和页高阈值中间,说明内存有一定压力,但还可以满足新内存请求。 剩余内存大于页高阈值,说明剩余内存比较多,没有内存压力。 我们可以看到,一旦剩余内存小于页低阈值,就会触发内存的回收。这个页低阈值,其实可以通过内核选项 /proc/sys/vm/min_free_kbytes 来间接设置。min_free_kbytes 设置了页最小阈值,而其他两个阈值,都是根据页最小阈值计算生成的,计算方法如下 : 12pages_low = pages_min*5/4pages_high = pages_min*3/2 NUMA 与 Swap很多情况下,你明明发现了Swap升高,可是在分析系统的内存使用时,却很可能发现,系统剩余内存还多着呢。为什么剩余内存很多的情况下,也会发生 Swap 呢? 看到上面的标题,你应该已经想到了,这正是处理器的 NUMA (Non-Uniform Memory Access)架构导致的。 关于 NUMA,我在 CPU 模块中曾简单提到过。在 NUMA 架构下,多个处理器被划分到不同 Node 上,且每个 Node 都拥有自己的本地内存空间。 而同一个 Node 内部的内存空间,实际上又可以进一步分为不同的内存域(Zone),比如直接内存访问区(DMA)、普通内存区(NORMAL)、伪内存区(MOVABLE)等,如下图所示: 先不用特别关注这些内存域的具体含义,我们只要会查看阈值的配置,以及缓存、匿名页的实际使用情况就够了。 既然 NUMA 架构下的每个 Node 都有自己的本地内存空间,那么,在分析内存的使用时,我们也应该针对每个 Node 单独分析。 你可以通过 numactl 命令,来查看处理器在 Node 的分布情况,以及每个 Node 的内存使用情况。比如,下面就是一个 numactl 输出的示例: 123456$ numactl --hardwareavailable: 1 nodes (0)node 0 cpus: 0 1node 0 size: 7977 MBnode 0 free: 4416 MB... 这个界面显示,我的系统中只有一个 Node,也就是 Node 0 ,而且编号为 0 和 1 的两个 CPU, 都位于 Node 0 上。另外,Node 0 的内存大小为 7977 MB,剩余内存为 4416 MB。 了解了 NUNA 的架构和 NUMA 内存的查看方法后,你可能就要问了这跟 Swap 有什么关系呢? 实际上,前面提到的三个内存阈值(页最小阈值、页低阈值和页高阈值),都可以通过内存域在 proc 文件系统中的接口 /proc/zoneinfo 来查看。 比如,下面就是一个 /proc/zoneinfo 文件的内容示例: 123456789101112131415$ cat /proc/zoneinfo...Node 0, zone Normal pages free 227894 min 14896 low 18620 high 22344... nr_free_pages 227894 nr_zone_inactive_anon 11082 nr_zone_active_anon 14024 nr_zone_inactive_file 539024 nr_zone_active_file 923986... 这个输出中有大量指标,我来解释一下比较重要的几个。 pages 处的 min、low、high,就是上面提到的三个内存阈值,而 free 是剩余内存页数,它跟后面的 nr_free_pages 相同。 nr_zone_active_anon 和 nr_zone_inactive_anon,分别是活跃和非活跃的匿名页数。 nr_zone_active_file 和 nr_zone_inactive_file,分别是活跃和非活跃的文件页数。 从这个输出结果可以发现,剩余内存远大于页高阈值,所以此时的 kswapd0 不会回收内存。 当然,某个 Node 内存不足时,系统可以从其他 Node 寻找空闲内存,也可以从本地内存中回收内存。具体选哪种模式,你可以通过 /proc/sys/vm/zone_reclaim_mode 来调整。它支持以下几个选项: 默认的 0 ,也就是刚刚提到的模式,表示既可以从其他 Node 寻找空闲内存,也可以从本地回收内存。 1、2、4 都表示只回收本地内存,2 表示可以回写脏数据回收内存,4 表示可以用 Swap 方式回收内存。 123456789101112131415161718vm.zone_reclaim_mode设置方法:echo 0 > /proc/sys/vm/zone_reclaim_mode,或sysctl -w vm.zone_reclaim_mode=0,或编辑/etc/sysctl.conf文件,加入vm.zone_reclaim_mode=0# echo 0 > /proc/sys/vm/zone_reclaim_mode# # 意味着关闭zone_reclaim模式,可以从其他zone或NUMA节点回收内存# echo 1 > /proc/sys/vm/zone_reclaim_mode# # 表示打开zone_reclaim模式,这样内存回收只会发生在本地节点内# echo 2 > /proc/sys/vm/zone_reclaim_mode# # 在本地回收内存时,可以将cache中的脏数据写回硬盘,以回收内存。# echo 4 > /proc/sys/vm/zone_reclaim_mode# # 可以用swap方式回收内存。 swappiness到这里,我们就可以理解内存回收的机制了。这些回收的内存既包括了文件页,又包括了匿名页。 对文件页的回收,当然就是直接回收缓存,或者把脏页写回磁盘后再回收。 而对匿名页的回收,其实就是通过 Swap 机制,把它们写入磁盘后再释放内存。 不过,你可能还有一个问题。既然有两种不同的内存回收机制,那么在实际回收内存时,到底该先回收哪一种呢? 其实,Linux 提供了一个 /proc/sys/vm/swappiness 选项,用来调整使用 Swap 的积极程度。 swappiness 的范围是 0-100,数值越大,越积极使用 Swap,也就是更倾向于回收匿名页;数值越小,越消极使用 Swap,也就是更倾向于回收文件页。 虽然 swappiness 的范围是 0-100,不过要注意,这并不是内存的百分比,而是调整 Swap 积极程度的权重,即使你把它设置成 0,当剩余内存 + 文件页小于页高阈值(https://www.kernel.org/doc/Documentation/sysctl/vm.txt)时,还是会发生 Swap。 清楚了 Swap 原理后,当遇到 Swap 使用变高时,又该怎么定位、分析呢?别急,下一节,我们将用一个案例来探索实践。 小结在内存资源紧张时,Linux 通过直接内存回收和定期扫描的方式,来释放文件页和匿名页,以便把内存分配给更需要的进程使用。 文件页的回收比较容易理解,直接清空,或者把脏数据写回磁盘后再释放。 而对匿名页的回收,需要通过 Swap 换出到磁盘中,下次访问时,再从磁盘换入到内存中。 你可以设置 /proc/sys/vm/min_free_kbytes,来调整系统定期回收内存的阈值(也就是页低阈值),还可以设置 /proc/sys/vm/swappiness,来调整文件页和匿名页的回收倾向。 在 NUMA 架构下,每个 Node都有自己的本地内存空间,而当本地内存不足时,默认既可以从其他 Node 寻找空闲内存,也可以从本地内存回收。 你可以设置 /proc/sys/vm/zone_reclaim_mode 来调整 NUMA 本地内存的回收策略。 程序被oom-kill很快系统内存就会被耗尽,进而触发 OOM killer 去杀进程。这个信息可以通过 dmesg(该命令是用来查看内核日志的)这个命令来查看: 1234567dmesgoom_adj 的范围是 [-17, 15],数值越大,表示进程越容易被 OOM 杀死;数值越小,表示进程越不容易被 OOM 杀死,其中 -17 表示禁止 OOM。echo -17 > /proc/$(pidof ele-vue)/oom_adj$ sudo sh -c \"echo -17 > /proc/$(pidof ele-vue)/oom_adj\" 开启 Swap 后,你可以设置 /proc/sys/vm/min_free_kbytes来调整系统定期回收内存的阈值,也可以设置 /proc/sys/vm/swappiness ,来调整文件页和匿名页的回收倾向。 1echo 0 >/proc/sys/vm/swappiness 永久修改: 在 /etc/sysctl.conf 文件添加 ”vm.swappiness=0” 行 脏页(应用程序修改过但暂时未写入磁盘的数据)的数据处理(启用内核线程 pdflush 负责这些脏页的刷新)/proc/sys/vm/nr_pdflush_threads 1vm.nr_pdflush_threads=2 kbdirty 就是系统中的脏页大小,它同样也是对 /proc/vmstat 中 nr_dirty 的解析。你可以通过调小如下设置来将系统脏页个数控制在一个合理范围: 12345vm.dirty_background_bytes=0vm.dirty_background_ratio=10vm.dirty_bytes=0vm.dirty_expire_centisecs=3000vm.dirty_ratio=20 调整这些配置项有利有弊,调大这些值会导致脏页的积压,但是同时也可能减少了 I/O 的次数,从而提升单次刷盘的效率;调小这些值可以减少脏页的积压,但是同时也增加了 I/O 的次数,降低了 I/O 的效率。 12345678910vm.min_free_kbytes=409600(来调整系统定期回收内存的阈值)vm.vfs_cache_pressure=200#加大这个参数设置了虚拟内存回收directory和i-node缓冲的倾向,这个值越大,回收的倾向越严重。调整这3个参数的目的就是让操作系统在平时就尽快回收缓冲,释放物理内存,这样就可以避免突发性的大规模换页。vm.overcommit_memory=1(表示即使内存耗尽也不杀死任何进程)#查看sysctl -a","tags":[]},{"title":"深入了解Synchronized同步锁的优化方法","date":"2022-08-09T07:17:52.000Z","path":"2022/08/09/lock-03/","text":"Lock 同步锁是基于 Java 实现的,而 Synchronized 是基于底层操作系统的 Mutex Lock 实现的,每次获取和释放锁操作都会带来用户态和内核态的切换,从而增加系统性能开销。因此,在锁竞争激烈的情况下,Synchronized 同步锁在性能上就表现得非常糟糕,它也常被大家称为重量级锁。 特别是在单个线程重复申请锁的情况下,JDK1.5 版本的 Synchronized 锁性能要比 Lock 的性能差很多。例如,在 Dubbo 基于 Netty 实现的通信中,消费端向服务端通信之后,由于接收返回消息是异步,所以需要一个线程轮询监听返回信息。而在接收消息时,就需要用到锁来确保 request session 的原子性。如果我们这里使用 Synchronized 同步锁,那么每当同一个线程请求锁资源时,都会发生一次用户态和内核态的切换。 到了 JDK1.6 版本之后,Java 对 Synchronized 同步锁做了充分的优化,甚至在某些场景下,它的性能已经超越了 Lock 同步锁。这一讲我们就来看看 Synchronized 同步锁究竟是通过了哪些优化,实现了性能地提升。 同步锁实现原理通常 Synchronized 实现同步锁的方式有两种,一种是修饰方法,一种是修饰方法块。以下就是通过 Synchronized 实现的两种同步方法加锁的方式: 12345678910111213// 关键字在实例方法上,锁为当前实例 public synchronized void method1() { // code } // 关键字在代码块上,锁为括号里面的对象 public void method2() { Object o = new Object(); synchronized (o) { // code } } 下面我们可以通过反编译看下具体字节码的实现,运行以下反编译命令,就可以输出我们想要的字节码: 1234javac -encoding UTF-8 SyncTest.java //先运行编译class文件命令javap -v SyncTest.class //再通过javap打印出字节文件 通过输出的字节码,你会发现:Synchronized 在修饰同步代码块时,是由 monitorenter 和 monitorexit 指令来实现同步的。进入 monitorenter 指令后,线程将持有 Monitor 对象,退出 monitorenter 指令后,线程将释放该 Monitor 对象。 123456789101112131415161718192021222324252627282930313233343536373839public void method2(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=4, args_size=1 0: new #2 3: dup 4: invokespecial #1 7: astore_1 8: aload_1 9: dup 10: astore_2 11: monitorenter //monitorenter 指令 12: aload_2 13: monitorexit //monitorexit 指令 14: goto 22 17: astore_3 18: aload_2 19: monitorexit 20: aload_3 21: athrow 22: return Exception table: from to target type 12 14 17 any 17 20 17 any LineNumberTable: line 18: 0 line 19: 8 line 21: 12 line 22: 22 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 17 locals = [ class com/demo/io/SyncTest, class java/lang/Object, class java/lang/Object ] stack = [ class java/lang/Throwable ] frame_type = 250 /* chop */ offset_delta = 4 再来看以下同步方法的字节码,你会发现:当 Synchronized 修饰同步方法时,并没有发现 monitorenter 和 monitorexit 指令,而是出现了一个 ACC_SYNCHRONIZED 标志。 这是因为 JVM 使用了 ACC_SYNCHRONIZED 访问标志来区分一个方法是否是同步方法。当方法调用时,调用指令将会检查该方法是否被设置 ACC_SYNCHRONIZED 访问标志。如果设置了该标志,执行线程将先持有 Monitor 对象,然后再执行方法。在该方法运行期间,其它线程将无法获取到该 Mointor 对象,当方法执行完成后,再释放该 Monitor 对象。 12345678public synchronized void method1(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED // ACC_SYNCHRONIZED 标志 Code: stack=0, locals=1, args_size=1 0: return LineNumberTable: line 8: 0 通过以上的源码,我们再来看看 Synchronized 修饰方法是怎么实现锁原理的。 JVM 中的同步是基于进入和退出管程(Monitor)对象实现的。每个对象实例都会有一个 Monitor,Monitor 可以和对象一起创建、销毁。Monitor 是由 ObjectMonitor 实现,而 ObjectMonitor 是由 C++ 的 ObjectMonitor.hpp 文件实现,如下所示: 12345678910111213141516171819202122232425262728293031323334353637ObjectMonitor() { _header = NULL; _count = 0; //记录个数 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ;}ObjectMonitor() { _header = NULL; _count = 0; //记录个数 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ;} 当多个线程同时访问一段同步代码时,多个线程会先被存放在 ContentionList 和 _EntryList 集合中,处于 block 状态的线程,都会被加入到该列表。接下来当线程获取到对象的 Monitor 时,Monitor 是依靠底层操作系统的 Mutex Lock 来实现互斥的,线程申请 Mutex 成功,则持有该 Mutex,其它线程将无法获取到该 Mutex,竞争失败的线程会再次进入 ContentionList 被挂起。 如果线程调用 wait() 方法,就会释放当前持有的 Mutex,并且该线程会进入 WaitSet 集合中,等待下一次被唤醒。如果当前线程顺利执行完方法,也将释放 Mutex。 多线程之线程池 CPU 密集型任务:这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。 当线程数量太小,同一时间大量请求将被阻塞在线程队列中排队等待执行线程,此时 CPU 没有得到充分利用;当线程数量太大,被创建的执行线程同时在争取 CPU 资源,又会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。通过测试可知,4~6 个线程数是最合适的。 I/O 密集型任务:这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。 备注:由于测试代码读取 2MB 大小的文件,涉及到大内存,所以在运行之前,我们需要调整 JVM 的堆内存空间:-Xms4g -Xmx4g,避免发生频繁的 FullGC,影响测试结果。 通过测试结果,我们可以看到每个线程所花费的时间。当线程数量在 8 时,线程平均执行时间是最佳的,这个线程数量和我们的计算公式所得的结果就差不多。 看完以上两种情况下的线程计算方法,你可能还想说,在平常的应用场景中,我们常常遇不到这两种极端情况,那么碰上一些常规的业务操作,比如,通过一个线程池实现向用户定时推送消息的业务,我们又该如何设置线程池的数量呢? 此时我们可以参考以下公式来计算线程数: 1线程数=N(CPU核数)*(1+WT(线程等待时间)/ST(线程时间运行时间)) 我们可以通过 JDK 自带的工具 VisualVM 来查看 WT/ST 比例,以下例子是基于运行纯 CPU 运算的例子,我们可以看到: 12WT(线程等待时间)= 36788ms [线程运行总时间] - 36788ms[ST(线程时间运行时间)]= 0线程数=N(CPU核数)*(1+ 0 [WT(线程等待时间)]/36788ms[ST(线程时间运行时间)])= N(CPU核数) 综合来看,我们可以根据自己的业务场景,从“N+1”和“2N”两个公式中选出一个适合的,计算出一个大概的线程数量,之后通过实际压测,逐渐往“增大线程数量”和“减小线程数量”这两个方向调整,然后观察整体的处理时间变化,最终确定一个具体的线程数量。","tags":[]},{"title":"多线程之锁优化之使用乐观锁优化并行操作","date":"2022-08-09T07:17:02.000Z","path":"2022/08/09/lock-02/","text":"乐观锁乐观锁,顾名思义,就是说在操作共享资源时,它总是抱着乐观的态度进行,它认为自己可以成功地完成操作。但实际上,当多个线程同时操作一个共享资源时,只有一个线程会成功,那么失败的线程呢?它们不会像悲观锁一样在操作系统中挂起,而仅仅是返回,并且系统允许失败的线程重试,也允许自动放弃退出操作。 所以,乐观锁相比悲观锁来说,不会带来死锁、饥饿等活性故障问题,线程间的相互影响也远远比悲观锁要小。更为重要的是,乐观锁没有因竞争造成的系统开销,所以在性能上也是更胜一筹。 乐观锁的实现原理CAS 是实现乐观锁的核心算法,它包含了 3 个参数:V(需要更新的变量)、E(预期值)和 N(最新值)。 只有当需要更新的变量等于预期值时,需要更新的变量才会被设置为最新值,如果更新值和预期值不同,则说明已经有其它线程更新了需要更新的变量,此时当前线程不做操作,返回 V 的真实值。 1.CAS 如何实现原子操作在 JDK 中的 concurrent 包中,atomic 路径下的类都是基于 CAS 实现的。AtomicInteger 就是基于 CAS 实现的一个线程安全的整型类。下面我们通过源码来了解下如何使用 CAS 实现原子操作。 我们可以看到 AtomicInteger 的自增方法 getAndIncrement 是用了 Unsafe 的 getAndAddInt 方法,显然 AtomicInteger 依赖于本地方法 Unsafe 类,Unsafe 类中的操作方法会调用 CPU 底层指令实现原子操作。 12345678910111213//基于CAS操作更新值 public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } //基于CAS操作增1 public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } //基于CAS操作减1 public final int getAndDecrement() { return unsafe.getAndAddInt(this, valueOffset, -1); 2. 处理器如何实现原子操作CAS 是调用处理器底层指令来实现原子操作,那么处理器底层又是如何实现原子操作的呢? 处理器和物理内存之间的通信速度要远慢于处理器间的处理速度,所以处理器有自己的内部缓存。如下图所示,在执行操作时,频繁使用的内存数据会缓存在处理器的 L1、L2 和 L3 高速缓存中,以加快频繁读取的速度。 一般情况下,一个单核处理器能自我保证基本的内存操作是原子性的,当一个线程读取一个字节时,所有进程和线程看到的字节都是同一个缓存里的字节,其它线程不能访问这个字节的内存地址。 但现在的服务器通常是多处理器,并且每个处理器都是多核的。每个处理器维护了一块字节的内存,每个内核维护了一块字节的缓存,这时候多线程并发就会存在缓存不一致的问题,从而导致数据不一致。 这个时候,处理器提供了总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。这个时候,处理器提供了总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。 当处理器要操作一个共享变量的时候,其在总线上会发出一个 Lock 信号,这时其它处理器就不能操作共享变量了,该处理器会独享此共享内存中的变量。但总线锁定在阻塞其它处理器获取该共享变量的操作请求时,也可能会导致大量阻塞,从而增加系统的性能开销。 于是,后来的处理器都提供了缓存锁定机制,也就说当某个处理器对缓存中的共享变量进行了操作,就会通知其它处理器放弃存储该共享资源或者重新读取该共享资源。目前最新的处理器都支持缓存锁定机制。 优化 CAS 乐观锁虽然乐观锁在并发性能上要比悲观锁优越,但是在写大于读的操作场景下,CAS 失败的可能性会增大,如果不放弃此次 CAS 操作,就需要循环做 CAS 重试,这无疑会长时间地占用 CPU。 在 JDK1.8 中,Java 提供了一个新的原子类 LongAdder。LongAdder 在高并发场景下会比 AtomicInteger 和 AtomicLong 的性能更好,代价就是会消耗更多的内存空间。 LongAdder 的原理就是降低操作共享变量的并发数,也就是将对单一共享变量的操作压力分散到多个变量值上,将竞争的每个写线程的 value 值分散到一个数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的 value 值进行 CAS 操作,最后在读取值的时候会将原子操作的共享变量与各个分散在数组的 value 值相加,返回一个近似准确的数值。 LongAdder 内部由一个 base 变量和一个 cell[]数组组成。当只有一个写线程,没有竞争的情况下,LongAdder 会直接使用 base 变量作为原子操作变量,通过 CAS 操作修改变量;当有多个写线程竞争的情况下,除了占用 base 变量的一个写线程之外,其它各个线程会将修改的变量写入到自己的槽 cell[]数组中,最终结果可通过以下公式计算得出: 我们可以发现,LongAdder 在操作后的返回值只是一个近似准确的数值,但是 LongAdder 最终返回的是一个准确的数值, 所以在一些对实时性要求比较高的场景下,LongAdder 并不能取代 AtomicInteger 或 AtomicLong。 通过以上结果,我们可以发现:在读大于写的场景下,读写锁 ReentrantReadWriteLock、StampedLock 以及乐观锁的读写性能是最好的;在写大于读的场景下,乐观锁的性能是最好的,其它 4 种锁的性能则相差不多;在读和写差不多的场景下,两种读写锁以及乐观锁的性能要优于 Synchronized 和 ReentrantLock。 源码:https://github.com/nickliuchao/lockTest/archive/refs/heads/master.zip","tags":[]},{"title":"深入了解Lock同步锁的优化方法","date":"2022-08-09T07:16:21.000Z","path":"2022/08/09/lock/","text":"相对于需要 JVM 隐式获取和释放锁的 Synchronized 同步锁,Lock 同步锁(以下简称 Lock 锁)需要的是显示获取和释放锁,这就为获取和释放锁提供了更多的灵活性。Lock 锁的基本操作是通过乐观锁来实现的,但由于 Lock 锁也会在阻塞时被挂起,因此它依然属于悲观锁。我们可以通过一张图来简单对比下两个同步锁,了解下各自的特点: 从性能方面上来说,在并发量不高、竞争不激烈的情况下,Synchronized 同步锁由于具有分级锁的优势,性能上与 Lock 锁差不多;但在高负载、高并发的情况下,Synchronized 同步锁由于竞争激烈会升级到重量级锁,性能则没有 Lock 锁稳定。 我们可以通过一组简单的性能测试,直观地对比下两种锁的性能,结果见下方,代码可以在Github(http://github.com/nickliuchao/syncLockTest)上下载查看。 Lock锁实现原理Lock 锁是基于 Java 实现的锁,Lock 是一个接口类,常用的实现类有 ReentrantLock、ReentrantReadWriteLock(RRW),它们都是依赖 AbstractQueuedSynchronizer(AQS)类实现的。 AQS 类结构中包含一个基于链表实现的等待队列(CLH 队列),用于存储所有阻塞的线程,AQS 中还有一个 state 变量,该变量对 ReentrantLock 来说表示加锁状态。 该队列的操作均通过 CAS 操作实现,我们可以通过一张图来看下整个获取锁的流程。 锁分离优化 Lock 同步锁虽然 Lock 锁的性能稳定,但也并不是所有的场景下都默认使用 ReentrantLock 独占锁来实现线程同步。 我们知道,对于同一份数据进行读写,如果一个线程在读数据,而另一个线程在写数据,那么读到的数据和最终的数据就会不一致;如果一个线程在写数据,而另一个线程也在写数据,那么线程前后看到的数据也会不一致。这个时候我们可以在读写方法中加入互斥锁,来保证任何时候只能有一个线程进行读或写操作。 在大部分业务场景中,读业务操作要远远大于写业务操作。而在多线程编程中,读操作并不会修改共享资源的数据,如果多个线程仅仅是读取共享资源,那么这种情况下其实没有必要对资源进行加锁。如果使用互斥锁,反倒会影响业务的并发性能,那么在这种场景下,有没有什么办法可以优化下锁的实现方式呢? 1. 读写锁 ReentrantReadWriteLock针对这种读多写少的场景,Java 提供了另外一个实现 Lock 接口的读写锁 RRW。我们已知 ReentrantLock 是一个独占锁,同一时间只允许一个线程访问,而 RRW 允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。读写锁内部维护了两个锁,一个是用于读操作的 ReadLock,一个是用于写操作的 WriteLock。 那读写锁又是如何实现锁分离来保证共享资源的原子性呢? RRW 也是基于 AQS 实现的,它的自定义同步器(继承 AQS)需要在同步状态 state 上维护多个读线程和一个写线程的状态,该状态的设计成为实现读写锁的关键。RRW 很好地使用了高低位,来实现一个整型控制两种状态的功能,读写锁将变量切分成了两个部分,高 16 位表示读,低 16 位表示写。 一个线程尝试获取写锁时,会先判断同步状态 state 是否为 0。如果 state 等于 0,说明暂时没有其它线程获取锁;如果 state 不等于 0,则说明有其它线程获取了锁。 此时再判断同步状态 state 的低 16 位(w)是否为 0,如果 w 为 0,则说明其它线程获取了读锁,此时进入 CLH 队列进行阻塞等待;如果 w 不为 0,则说明其它线程获取了写锁,此时要判断获取了写锁的是不是当前线程,若不是就进入 CLH 队列进行阻塞等待;若是,就应该判断当前线程获取写锁是否超过了最大次数,若超过,抛异常,反之更新同步状态。 一个线程尝试获取读锁时,同样会先判断同步状态 state 是否为 0。如果 state 等于 0,说明暂时没有其它线程获取锁,此时判断是否需要阻塞,如果需要阻塞,则进入 CLH 队列进行阻塞等待;如果不需要阻塞,则 CAS 更新同步状态为读状态。 如果 state 不等于 0,会判断同步状态低 16 位,如果存在写锁,则获取读锁失败,进入 CLH 阻塞队列;反之,判断当前线程是否应该被阻塞,如果不应该阻塞则尝试 CAS 同步状态,获取成功更新同步锁为读状态。 下面我们通过一个求平方的例子,来感受下 RRW 的实现,代码如下: 1234567891011121314151617181920212223242526272829303132333435public class TestRTTLock { private double x, y; private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); // 读锁 private Lock readLock = lock.readLock(); // 写锁 private Lock writeLock = lock.writeLock(); public double read() { //获取读锁 readLock.lock(); try { return Math.sqrt(x * x + y * y); } finally { //释放读锁 readLock.unlock(); } } public void move(double deltaX, double deltaY) { //获取写锁 writeLock.lock(); try { x += deltaX; y += deltaY; } finally { //释放写锁 writeLock.unlock(); } }} 2. 读写锁再优化之 StampedLockRRW 被很好地应用在了读大于写的并发场景中,然而 RRW 在性能上还有可提升的空间。在读取很多、写入很少的情况下,RRW 会使写入线程遭遇饥饿(Starvation)问题,也就是说写入线程会因迟迟无法竞争到锁而一直处于等待状态。 在 JDK1.8 中,Java 提供了 StampedLock 类解决了这个问题。StampedLock 不是基于 AQS 实现的,但实现的原理和 AQS 是一样的,都是基于队列和锁状态实现的。与 RRW 不一样的是,StampedLock 控制锁有三种模式: 写、悲观读以及乐观读,并且 StampedLock 在获取锁时会返回一个票据 stamp,获取的 stamp 除了在释放锁时需要校验,在乐观读模式下,stamp 还会作为读取共享资源后的二次校验,后面我会讲解 stamp 的工作原理。 我们先通过一个官方的例子来了解下 StampedLock 是如何使用的,代码如下: 123456789101112131415161718192021222324252627282930313233343536public class Point { private double x, y; private final StampedLock s1 = new StampedLock(); void move(double deltaX, double deltaY) { //获取写锁 long stamp = s1.writeLock(); try { x += deltaX; y += deltaY; } finally { //释放写锁 s1.unlockWrite(stamp); } } double distanceFormOrigin() { //乐观读操作 long stamp = s1.tryOptimisticRead(); //拷贝变量 double currentX = x, currentY = y; //判断读期间是否有写操作 if (!s1.validate(stamp)) { //升级为悲观读 stamp = s1.readLock(); try { currentX = x; currentY = y; } finally { s1.unlockRead(stamp); } } return Math.sqrt(currentX * currentX + currentY * currentY); }} 我们可以发现:一个写线程获取写锁的过程中,首先是通过 WriteLock 获取一个票据 stamp,WriteLock 是一个独占锁,同时只有一个线程可以获取该锁,当一个线程获取该锁后,其它请求的线程必须等待,当没有线程持有读锁或者写锁的时候才可以获取到该锁。请求该锁成功后会返回一个 stamp 票据变量,用来表示该锁的版本,当释放该锁的时候,需要 unlockWrite 并传递参数 stamp。 接下来就是一个读线程获取锁的过程。首先线程会通过乐观锁 tryOptimisticRead 操作获取票据 stamp ,如果当前没有线程持有写锁,则返回一个非 0 的 stamp 版本信息。线程获取该 stamp 后,将会拷贝一份共享资源到方法栈,在这之前具体的操作都是基于方法栈的拷贝数据。 之后方法还需要调用 validate,验证之前调用 tryOptimisticRead 返回的 stamp 在当前是否有其它线程持有了写锁,如果是,那么 validate 会返回 0,升级为悲观锁;否则就可以使用该 stamp 版本的锁对数据进行操作。 相比于 RRW,StampedLock 获取读锁只是使用与或操作进行检验,不涉及 CAS 操作,即使第一次乐观锁获取失败,也会马上升级至悲观锁,这样就可以避免一直进行 CAS 操作带来的 CPU 占用性能的问题,因此 StampedLock 的效率更高。 总结不管使用 Synchronized 同步锁还是 Lock 同步锁,只要存在锁竞争就会产生线程阻塞,从而导致线程之间的频繁切换,最终增加性能消耗。因此,如何降低锁竞争,就成为了优化锁的关键。 在 Synchronized 同步锁中,我们了解了可以通过减小锁粒度、减少锁占用时间来降低锁的竞争。在这一讲中,我们知道可以利用 Lock 锁的灵活性,通过锁分离的方式来降低锁竞争。 Lock 锁实现了读写锁分离来优化读大于写的场景,从普通的 RRW 实现到读锁和写锁,到 StampedLock 实现了乐观读锁、悲观读锁和写锁,都是为了降低锁的竞争,促使系统的并发性能达到最佳。","tags":[]},{"title":"JVM调优之JVM内存模型","date":"2022-08-09T07:14:34.000Z","path":"2022/08/09/jvm-02/","text":"JVM内存模型设计VM 自动内存分配管理机制的好处很多,但实则是把双刃剑。这个机制在提升 Java 开发效率的同时,也容易使 Java 开发人员过度依赖于自动化,弱化对内存的管理能力,这样系统就很容易发生 JVM 的堆内存异常,垃圾回收(GC)的方式不合适以及 GC 次数过于频繁等问题,这些都将直接影响到应用服务的性能。 因此,要进行 JVM 层面的调优,就需要深入了解 JVM 内存分配和回收原理,这样在遇到问题时,我们才能通过日志分析快速地定位问题;也能在系统遇到性能瓶颈时,通过分析 JVM 调优来优化系统性能。这也是整个模块四的重点内容,今天我们就从 JVM 的内存模型学起,为后续的学习打下一个坚实的基础。 我们先通过一张 JVM 内存模型图,来熟悉下其具体设计。在 Java 中,JVM 内存模型主要分为堆、程序计数器、方法区、虚拟机栈和本地方法栈。 1. 堆(Heap)堆是 JVM 内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 和 Survivor 区,最后 Survivor 由 From Survivor 和 To Survivor 组成。 在 Java6 版本中,永久代在非堆内存区;到了 Java7 版本,永久代的静态变量和运行时常量池被合并到了堆中;而到了 Java8,永久代被元空间取代了。 结构如下图所示: 2. 程序计数器(Program Counter Register)程序计数器是一块很小的内存空间,主要用来记录各个线程执行的字节码的地址,例如,分支、循环、跳转、异常、线程恢复等都依赖于计数器。 由于 Java 是多线程语言,当执行的线程数量超过 CPU 核数时,线程之间会根据时间片轮询争夺 CPU 资源。如果一个线程的时间片用完了,或者是其它原因导致这个线程的 CPU 资源被提前抢夺,那么这个退出的线程就需要单独的一个程序计数器,来记录下一条运行的指令。 3. 方法区(Method Area)方法区主要是用来存放已被虚拟机加载的类相关信息,包括类信息、运行时常量池、字符串常量池。类信息又包括了类的版本、字段、方法、接口和父类等信息。 JVM 在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。在加载类的时候,JVM 会先加载 class 文件,而在 class 文件中除了有类的版本、字段、方法和接口等描述信息外,还有一项信息是常量池 (Constant Pool Table),用于存放编译期间生成的各种字面量和符号引用。 字面量包括字符串(String a=“b”)、基本类型的常量(final 修饰的变量),符号引用则包括类和方法的全限定名(例如 String 这个类,它的全限定名就是 Java/lang/String)、字段的名称和描述符以及方法的名称和描述符。 而当类加载到内存中后,JVM 就会将 class 文件常量池中的内容存放到运行时的常量池中;在解析阶段,JVM 会把符号引用替换为直接引用(对象的索引值)。 例如,类中的一个字符串常量在 class 文件中时,存放在 class 文件常量池中的;在 JVM 加载完类之后,JVM 会将这个字符串常量放到运行时常量池中,并在解析阶段,指定该字符串对象的索引值。运行时常量池是全局共享的,多个类共用一个运行时常量池,class 文件中常量池多个相同的字符串在运行时常量池只会存在一份。 方法区与堆空间类似,也是一个共享内存区,所以方法区是线程共享的。假如两个线程都试图访问方法区中的同一个类信息,而这个类还没有装入 JVM,那么此时就只允许一个线程去加载它,另一个线程必须等待。 在 HotSpot 虚拟机、Java7 版本中已经将永久代的静态变量和运行时常量池转移到了堆中,其余部分则存储在 JVM 的非堆内存中,而 Java8 版本已经将方法区中实现的永久代去掉了,并用元空间(class metadata)代替了之前的永久代,并且元空间的存储位置是本地内存。之前永久代的类的元数据存储在了元空间,永久代的静态变量(class static variables)以及运行时常量池(runtime constant pool)则跟 Java7 一样,转移到了堆中。 那你可能又有疑问了,Java8 为什么使用元空间替代永久代,这样做有什么好处呢? 移除永久代是为了融合 HotSpot JVM 与 JRockit VM 而做出的努力,因为 JRockit 没有永久代,所以不需要配置永久代。 永久代内存经常不够用或发生内存溢出,爆出异常 java.lang.OutOfMemoryError: PermGen。这是因为在 JDK1.7 版本中,指定的 PermGen 区大小为 8M,由于 PermGen 中类的元数据信息在每次 FullGC 的时候都可能被收集,回收率都偏低,成绩很难令人满意;还有,为 PermGen 分配多大的空间很难确定,PermSize 的大小依赖于很多因素,比如,JVM 加载的 class 总数、常量池的大小和方法的大小等。 4. 虚拟机栈(VM stack)Java 虚拟机栈是线程私有的内存空间,它和 Java 线程一起创建。当创建一个线程时,会在虚拟机栈中申请一个线程栈,用来保存方法的局部变量、操作数栈、动态链接方法和返回地址等信息,并参与方法的调用和返回。每一个方法的调用都伴随着栈帧的入栈操作,方法的返回则是栈帧的出栈操作。 5. 本地方法栈(Native Method Stack)本地方法栈跟 Java 虚拟机栈的功能类似,Java 虚拟机栈用于管理 Java 函数的调用,而本地方法栈则用于管理本地方法的调用。但本地方法并不是用 Java 实现的,而是由 C 语言实现的。 JVM 的运行原理看到这里,相信你对 JVM 内存模型已经有个充分的了解了。接下来,我们通过一个案例来了解下代码和对象是如何分配存储的,Java 代码又是如何在 JVM 中运行的。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162public class JVMCase { // 常量 public final static String MAN_SEX_TYPE = \"man\"; // 静态变量 public static String WOMAN_SEX_TYPE = \"woman\"; public static void main(String[] args) { Student stu = new Student(); stu.setName(\"nick\"); stu.setSexType(MAN_SEX_TYPE); stu.setAge(20); JVMCase jvmcase = new JVMCase(); // 调用静态方法 print(stu); // 调用非静态方法 jvmcase.sayHello(stu); } // 常规静态方法 public static void print(Student stu) { System.out.println(\"name: \" + stu.getName() + \"; sex:\" + stu.getSexType() + \"; age:\" + stu.getAge()); } // 非静态方法 public void sayHello(Student stu) { System.out.println(stu.getName() + \"say: hello\"); }}class Student{ String name; String sexType; int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSexType() { return sexType; } public void setSexType(String sexType) { this.sexType = sexType; } public int getAge() { return age; } public void setAge(int age) { this.age = age; }} 当我们通过 Java 运行以上代码时,JVM 的整个处理过程如下: 1.JVM 向操作系统申请内存,JVM 第一步就是通过配置参数或者默认配置参数向操作系统申请内存空间,根据内存大小找到具体的内存分配表,然后把内存段的起始地址和终止地址分配给 JVM,接下来 JVM 就进行内部分配。 2.JVM 获得内存空间后,会根据配置参数分配堆、栈以及方法区的内存大小。 3.class 文件加载、验证、准备以及解析,其中准备阶段会为类的静态变量分配内存,初始化为系统的初始值(这部分我在第 21 讲还会详细介绍)。 完成上一个步骤后,将会进行最后一个初始化阶段。在这个阶段中,JVM 首先会执行构造器 方法,编译器会在.java 文件被编译成.class 文件时,收集所有类的初始化代码,包括静态变量赋值语句、静态代码块、静态方法,收集在一起成为 () 方法。 执行方法。启动 main 线程,执行 main 方法,开始执行第一行代码。此时堆内存中会创建一个 student 对象,对象引用 student 就存放在栈中。 此时再次创建一个 JVMCase 对象,调用 sayHello 非静态方法,sayHello 方法属于对象 JVMCase,此时 sayHello 方法入栈,并通过栈中的 student 引用调用堆中的 Student 对象;之后,调用静态方法 print,print 静态方法属于 JVMCase 类,是从静态方法中获取,之后放入到栈中,也是通过 student 引用调用堆中的 student 对象。","tags":[]},{"title":"如何优化垃圾回收机制","date":"2022-08-09T07:13:43.000Z","path":"2022/08/09/gc/","text":"面对不同的业务场景,垃圾回收的调优策略也不一样。例如,在对内存要求苛刻的情况下,需要提高对象的回收效率;在 CPU 使用率高的情况下,需要降低高并发时垃圾回收的频率。可以说,垃圾回收的调优是一项必备技能。 垃圾回收机制掌握 GC 算法之前,我们需要先弄清楚 3 个问题。第一,回收发生在哪里?第二,对象在什么时候可以被回收?第三,如何回收这些对象? 回收发生地JVM 的内存区域中,程序计数器、虚拟机栈和本地方法栈这 3 个区域是线程私有的,随着线程的创建而创建,销毁而销毁;栈中的栈帧随着方法的进入和退出进行入栈和出栈操作,每个栈帧中分配多少内存基本是在类结构确定下来的时候就已知的,因此这三个区域的内存分配和回收都具有确定性。 那么垃圾回收的重点就是关注堆和方法区中的内存了,堆中的回收主要是对象的回收,方法区的回收主要是废弃常量和无用的类的回收。 可回收时间那 JVM 又是怎样判断一个对象是可以被回收的呢?一般一个对象不再被引用,就代表该对象可以被回收。目前有以下两种算法可以判断该对象是否可以被回收。 引用计数算法:这种算法是通过一个对象的引用计数器来判断该对象是否被引用了。每当对象被引用,引用计数器就会加 1;每当引用失效,计数器就会减 1。当对象的引用计数器的值为 0 时,就说明该对象不再被引用,可以被回收了。这里强调一点,虽然引用计数算法的实现简单,判断效率也很高,但它存在着对象之间相互循环引用的问题。 可达性分析算法:GC Roots 是该算法的基础,GC Roots 是所有对象的根对象,在 JVM 加载时,会创建一些普通对象引用正常对象。这些对象作为正常对象的起始点,在垃圾回收时,会从这些 GC Roots 开始向下搜索,当一个对象到 GC Roots 没有任何引用链相连时,就证明此对象是不可用的。目前 HotSpot 虚拟机采用的就是这种算法。 如何回收这些对象自动性:Java 提供了一个系统级的线程来跟踪每一块分配出去的内存空间,当 JVM 处于空闲循环时,垃圾收集器线程会自动检查每一块分配出去的内存空间,然后自动回收每一块空闲的内存块。 不可预期性:一旦一个对象没有被引用了,该对象是否立刻被回收呢?答案是不可预期的。我们很难确定一个没有被引用的对象是不是会被立刻回收掉,因为有可能当程序结束后,这个对象仍在内存中。 垃圾回收线程在 JVM 中是自动执行的,Java 程序无法强制执行。我们唯一能做的就是通过调用 System.gc 方法来”建议”执行垃圾收集器,但是否可执行,什么时候执行?仍然不可预期。 GC算法JVM 提供了不同的回收算法来实现这一套回收机制,通常垃圾收集器的回收算法可以分为以下几种: 如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现,JDK1.7 update14 之后 Hotspot 虚拟机所有的回收器整理如下(以下为服务端垃圾收集器): 其实在 JVM 规范中并没有明确 GC 的运作方式,各个厂商可以采用不同的方式实现垃圾收集器。我们可以通过 JVM 工具查询当前 JVM 使用的垃圾收集器类型,首先通过 ps 命令查询出进程 ID,再通过 jmap -heap ID 查询出 JVM 的配置信息,其中就包括垃圾收集器的设置类型。 GC 性能衡量指标一个垃圾收集器在不同场景下表现出的性能也不一样,那么如何评价一个垃圾收集器的性能好坏呢?我们可以借助一些指标。 吞吐量、停顿时间、垃圾回收频率 查看 & 分析 GC 日志已知了性能衡量指标,现在我们需要通过工具查询 GC 相关日志,统计各项指标的信息。首先,我们需要通过 JVM 参数预先设置 GC 日志,通常有以下几种 JVM 参数设置: 123456-XX:+PrintGC 输出GC日志-XX:+PrintGCDetails 输出GC的详细日志-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息-Xloggc:../logs/gc.log 日志文件的输出路径 GC 调优策略1. 降低 Minor GC 频率通常情况下,由于新生代空间较小,Eden 区很快被填满,就会导致频繁 Minor GC,因此我们可以通过增大新生代空间来降低 Minor GC 的频率。 可能你会有这样的疑问,扩容 Eden 区虽然可以减少 Minor GC 的次数,但不会增加单次 Minor GC 的时间吗?如果单次 Minor GC 的时间增加,那也很难达到我们期待的优化效果呀。 我们知道,单次 Minor GC 时间是由两部分组成:T1(扫描新生代)和 T2(复制存活对象)。假设一个对象在 Eden 区的存活时间为 500ms,Minor GC 的时间间隔是 300ms,那么正常情况下,Minor GC 的时间为 :T1+T2。 当我们增大新生代空间,Minor GC 的时间间隔可能会扩大到 600ms,此时一个存活 500ms 的对象就会在 Eden 区中被回收掉,此时就不存在复制存活对象了,所以再发生 Minor GC 的时间为:两次扫描新生代,即 2T1。 可见,扩容后,Minor GC 时增加了 T1,但省去了 T2 的时间。通常在虚拟机中,复制对象的成本要远高于扫描成本。 如果在堆内存中存在较多的长期存活的对象,此时增加年轻代空间,反而会增加 Minor GC 的时间。如果堆中的短期对象很多,那么扩容新生代,单次 Minor GC 时间不会显著增加。因此,单次 Minor GC 时间更多取决于 GC 后存活对象的数量,而非 Eden 区的大小。 2. 降低 Full GC 的频率通常情况下,由于堆内存空间不足或老年代对象太多,会触发 Full GC,频繁的 Full GC 会带来上下文切换,增加系统的性能开销。我们可以使用哪些方法来降低 Full GC 的频率呢? 减少创建大对象:在平常的业务场景中,我们习惯一次性从数据库中查询出一个大对象用于 web 端显示。例如,我之前碰到过一个一次性查询出 60 个字段的业务操作,这种大对象如果超过年轻代最大对象阈值,会被直接创建在老年代;即使被创建在了年轻代,由于年轻代的内存空间有限,通过 Minor GC 之后也会进入到老年代。这种大对象很容易产生较多的 Full GC。 我们可以将这种大对象拆解出来,首次只查询一些比较重要的字段,如果还需要其它字段辅助查看,再通过第二次查询显示剩余的字段。 增大堆内存空间:在堆内存不足的情况下,增大堆内存空间,且设置初始化堆内存为最大堆内存,也可以降低 Full GC 的频率。 选择合适的 GC 回收器 假设我们有这样一个需求,要求每次操作的响应时间必须在 500ms 以内。这个时候我们一般会选择响应速度较快的 GC 回收器,CMS(Concurrent Mark Sweep)回收器和 G1 回收器都是不错的选择。 而当我们的需求对系统吞吐量有要求时,就可以选择 Parallel Scavenge 回收器来提高系统的吞吐量。 在 JDK1.8 环境下,默认使用的是 Parallel Scavenge(年轻代)+Serial Old(老年代)垃圾收集器,你可以通过文中介绍的查询 JVM 的 GC 默认配置方法进行查看。 通常情况,JVM 是默认垃圾回收优化的,在没有性能衡量标准的前提下,尽量避免修改 GC 的一些性能配置参数。如果一定要改,那就必须基于大量的测试结果或线上的具体性能来进行调整。","tags":[]},{"title":"SpringBoot设置事务隔离等级","date":"2022-08-09T07:12:02.000Z","path":"2022/08/09/spring-transaction/","text":"Spring Boot 使用事务非常简单,首先使用注解 @EnableTransactionManagement 开启事务支持后,然后在访问数据库的Service方法上添加注解 @Transactional 便可。(在下文中会有图例) 关于事务管理器,不管是JPA还是JDBC等都实现自接口 PlatformTransactionManager 如果你添加的是 spring-boot-starter-jdbc 依赖,框架会默认注入 DataSourceTransactionManager 实例。如果Spring容器中存在多个 PlatformTransactionManager 实例,并且没有实现接口 TransactionManagementConfigurer 指定默认值,在我们在方法上使用注解 @Transactional 的时候,就必须要用value指定,如果不指定,则会抛出异常。对于系统需要提供默认事务管理的情况下,实现接口 TransactionManagementConfigurer 指定。对有的系统,为了避免不必要的问题,在业务中必须要明确指定 @Transactional 的 value 值的情况下。不建议实现接口 TransactionManagementConfigurer,这样控制台会明确抛出异常,开发人员就不会忘记主动指定(这样也更方便的控制不同业务上使用事务)。 配置 在启动主类添加注解:@EnableTransactionManagement 来启用注解式事务管理,相当于之前在xml中配置的<tx:annotation-driven />注解驱动。 在需要事务的类或者方法(service)上面添加@Transactional() 注解,里面可以配置需要的粒度,如开头说到的,如果没有设置默认的事务等级,需要在此添加isolation和propagation属性,还有几个其他的属性可以设置,在此只介绍这两个比较重要的属性。 属性配置1、Isolation :隔离级别由低到高分别为Read uncommitted(脏读) 、Read committed(不可重复读与幻读) 、Repeatable read(可重复读)、Serializable 隔离级别是指若干个并发的事务之间的隔离程度,与我们开发时候主要相关的场景包括:脏读取、重复读、幻读。 DEFAULT :这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是: READ_COMMITTED 。 (默认transaction用的是数据库默认的隔离级别不是一定是RR,只是用MySQL默认是RR) READ_UNCOMMITTED :该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读和不可重复读,因此很少使用该隔离级别。 READ_COMMITTED :该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。 REPEATABLE_READ :该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。该级别可以防止脏读和不可重复读。 SERIALIZABLE :所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 指定方法:通过使用 isolation 属性设置,例如:@Transactional(isolation = Isolation.DEFAULT) 2、Propagation:传播行为所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。 REQUIRED :如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。 SUPPORTS :如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。 MANDATORY :如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。 REQUIRES_NEW :创建一个新的事务,如果当前存在事务,则把当前事务挂起。 NOT_SUPPORTED :以非事务方式运行,如果当前存在事务,则把当前事务挂起。 NEVER :以非事务方式运行,如果当前存在事务,则抛出异常。 NESTED :如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 REQUIRED 。 指定方法:通过使用 propagation 属性设置,例如:@Transactional(propagation = Propagation.REQUIRED) String事务是对数据库事务的一层封装,可以通过设置Spring事务来管理数据库事务,但不能仅仅依靠Spring事务,需要考虑事务隔离级别以及事务传播行为。 结合业务场景,使用低级别事务隔离 避免行锁升级表锁 控制事务的大小,减少锁定的资源量和锁定时间长度","tags":[]},{"title":"高并发场景下的数据库事务调优","date":"2022-08-09T07:10:50.000Z","path":"2022/08/09/Mysql-02/","text":"并发事务带来的问题 数据丢失 脏读 不可重复读 幻读 事务隔离解决并发问题以上 4 个并发事务带来的问题,其中,数据丢失可以基于数据库中的悲观锁来避免发生,即在查询时通过在事务中使用 select xx for update 语句来实现一个排他锁,保证在该事务结束之前其他事务无法更新该数据。 当然,我们也可以基于乐观锁来避免,即将某一字段作为版本号,如果更新时的版本号跟之前的版本一致,则更新,否则更新失败。剩下 3 个问题,其实是数据库读一致性造成的,需要数据库提供一定的事务隔离机制来解决。 我们通过加锁的方式,可以实现不同的事务隔离机制。在了解事务隔离机制之前,我们不妨先来了解下 MySQL 都有哪些锁机制。 InnoDB 实现了两种类型的锁机制:共享锁(S)和排他锁(X)。共享锁允许一个事务读数据,不允许修改数据,如果其他事务要再对该行加锁,只能加共享锁;排他锁是修改数据时加的锁,可以读取和修改数据,一旦一个事务对该行数据加锁,其他事务将不能再对该数据加任务锁。 熟悉了以上 InnoDB 行锁的实现原理,我们就可以更清楚地理解下面的内容。 在操作数据的事务中,不同的锁机制会产生以下几种不同的事务隔离级别,不同的隔离级别分别可以解决并发事务产生的几个问题,对应如下: 未提交读(Read Uncommitted):在事务 A 读取数据时,事务 B 读取数据加了共享锁,修改数据时加了排它锁。这种隔离级别,会导致脏读、不可重复读以及幻读。 已提交读(Read Committed):在事务 A 读取数据时增加了共享锁,一旦读取,立即释放锁,事务 B 读取修改数据时增加了行级排他锁,直到事务结束才释放锁。也就是说,事务 A 在读取数据时,事务 B 只能读取数据,不能修改。当事务 A 读取到数据后,事务 B 才能修改。这种隔离级别,可以避免脏读,但依然存在不可重复读以及幻读的问题。 可重复读(Repeatable Read):在事务 A 读取数据时增加了共享锁,事务结束,才释放锁,事务 B 读取修改数据时增加了行级排他锁,直到事务结束才释放锁。也就是说,事务 A 在没有结束事务时,事务 B 只能读取数据,不能修改。当事务 A 结束事务,事务 B 才能修改。这种隔离级别,可以避免脏读、不可重复读,但依然存在幻读的问题。 可序列化(Serializable):在事务 A 读取数据时增加了共享锁,事务结束,才释放锁,事务 B 读取修改数据时增加了表级排他锁,直到事务结束才释放锁。可序列化解决了脏读、不可重复读、幻读等问题,但隔离级别越来越高的同时,并发性会越来越低。 InnoDB 中的 RC 和 RR 隔离事务是基于多版本并发控制(MVCC)实现高性能事务。一旦数据被加上排他锁,其他事务将无法加入共享锁,且处于阻塞等待状态,如果一张表有大量的请求,这样的性能将是无法支持的。 MVCC 对普通的 Select 不加锁,如果读取的数据正在执行 Delete 或 Update 操作,这时读取操作不会等待排它锁的释放,而是直接利用 MVCC 读取该行的数据快照(数据快照是指在该行的之前版本的数据,而数据快照的版本是基于 undo 实现的,undo 是用来做事务回滚的,记录了回滚的不同版本的行记录)。MVCC 避免了对数据重复加锁的过程,大大提高了读操作的性能。 锁具体实现算法我们知道,InnoDB 既实现了行锁,也实现了表锁。行锁是通过索引实现的,如果不通过索引条件检索数据,那么 InnoDB 将对表中所有的记录进行加锁,其实就是升级为表锁了。 行锁的具体实现算法有三种:record lock、gap lock 以及 next-key lock。record lock 是专门对索引项加锁;gap lock 是对索引项之间的间隙加锁;next-key lock 则是前面两种的组合,对索引项以其之间的间隙加锁。 只在可重复读或以上隔离级别下的特定操作才会取得 gap lock 或 next-key lock,在 Select 、Update 和 Delete 时,除了基于唯一索引的查询之外,其他索引查询时都会获取 gap lock 或 next-key lock,即锁住其扫描的范围。 优化高并发事务1. 结合业务场景,使用低级别事务隔离在高并发业务中,为了保证业务数据的一致性,操作数据库时往往会使用到不同级别的事务隔离。隔离级别越高,并发性能就越低。 那换到业务场景中,我们如何判断用哪种隔离级别更合适呢?我们可以通过两个简单的业务来说下其中的选择方法。 我们在修改用户最后登录时间的业务场景中,这里对查询用户的登录时间没有特别严格的准确性要求,而修改用户登录信息只有用户自己登录时才会修改,不存在一个事务提交的信息被覆盖的可能。所以我们允许该业务使用最低隔离级别。 而如果是账户中的余额或积分的消费,就存在多个客户端同时消费一个账户的情况,此时我们应该选择 RR 级别来保证一旦有一个客户端在对账户进行消费,其他客户端就不可能对该账户同时进行消费了。 2. 避免行锁升级表锁前面讲了,在 InnoDB 中,行锁是通过索引实现的,如果不通过索引条件检索数据,行锁将会升级到表锁。我们知道,表锁是会严重影响到整张表的操作性能的,所以我们应该避免他。 3. 控制事务的大小,减少锁定的资源量和锁定时间长度你是否遇到过以下 SQL 异常呢?在抢购系统的日志中,在活动区间,我们经常可以看到这种异常日志: 1MySQLQueryInterruptedException: Query execution was interrupted 由于在抢购提交订单中开启了事务,在高并发时对一条记录进行更新的情况下,由于更新记录所在的事务还可能存在其他操作,导致一个事务比较长,当有大量请求进入时,就可能导致一些请求同时进入到事务中。 又因为锁的竞争是不公平的,当多个事务同时对一条记录进行更新时,极端情况下,一个更新操作进去排队系统后,可能会一直拿不到锁,最后因超时被系统打断踢出。 在用户购买商品时,首先我们需要查询库存余额,再新建一个订单,并扣除相应的库存。这一系列操作是处于同一个事务的。 以上业务若是在两种不同的执行顺序下,其结果都是一样的,但在事务性能方面却不一样: 这是因为,虽然这些操作在同一个事务,但锁的申请在不同时间,只有当其他操作都执行完,才会释放所有锁。因为扣除库存是更新操作,属于行锁,这将会影响到其他操作该数据的事务,所以我们应该尽量避免长时间地持有该锁,尽快释放该锁。 又因为先新建订单和先扣除库存都不会影响业务,所以我们可以将扣除库存操作放到最后,也就是使用执行顺序 1,以此尽量减小锁的持有时间。 总结其实 MySQL 的并发事务调优和 Java 的多线程编程调优非常类似,都是可以通过减小锁粒度和减少锁的持有时间进行调优。在 MySQL 的并发事务调优中,我们尽量在可以使用低事务隔离级别的业务场景中,避免使用高事务隔离级别。 在功能业务开发时,开发人员往往会为了追求开发速度,习惯使用默认的参数设置来实现业务功能。例如,在 service 方法中,你可能习惯默认使用 transaction,很少再手动变更事务隔离级别。但要知道,transaction 默认是 RR 事务隔离级别,在某些业务场景下,可能并不合适。因此,我们还是要结合具体的业务场景,进行考虑。 思考题InnoDB 是如何实现原子性、一致性和持久性的吗? binlog + redo log 两阶段提交保证持久性事务的回滚机制 保证原子性 要么全部提交成功 要么回滚undo log + MVCC 保证一致性 事务开始和结束的过程不会其它事务看到 为了并发可以适当破坏一致性","tags":[]},{"title":"如何写出高性能SQL语句","date":"2022-08-09T07:09:59.000Z","path":"2022/08/09/Mysql-01/","text":"锁共享锁(Share Lock)共享锁又称读锁,简称S锁;当一个事务为数据加上读锁之后,其他事务只能对该数据加读锁,而不能对数据加写锁,直到所有的读锁释放之后其他事务才能对其进行加持写锁。 共享锁的特性主要是为了支持并发的读取数据,读取数据的时候不支持修改,避免出现重复读的问题。 排他锁(eXclusive Lock)排他锁又称写锁,简称X锁;当一个事务为数据加上写锁时,其他请求将不能再为数据加任何锁,直到该锁释放之后,其他事务才能对数据进行加锁。 排他锁的目的是在数据修改时候,不允许其他人同时修改,也不允许其他人读取。避免了出现脏数据和脏读的问题。 慢 SQL 语句的几种常见诱因 无索引、索引失效导致慢查询 锁等待 不恰当的 SQL 语句 使用不恰当的 SQL 语句也是慢 SQL 最常见的诱因之一。例如,习惯使用SQL 语句,在大数据表中使用 分页查询,以及对非索引字段进行排序等等。 优化 SQL 语句的步骤1. 通过 EXPLAIN 分析 SQL 执行计划 select_type:表示 SELECT 查询类型,常见的有 SIMPLE(普通查询,即没有联合查询、子查询)、PRIMARY(主查询)、UNION(UNION 中后面的查询)、SUBQUERY(子查询)等。 type:表示从表中查询到行所执行的方式,查询方式是 SQL 优化中一个很重要的指标,结果值从好到差依次是:system > const > eq_ref > ref > range > index > ALL。 possible_keys:可能使用到的索引。 key:实际使用到的索引。 key_len:当前使用的索引的长度。ref:关联 id 等信息。 rows:查找到记录所扫描的行数。 filtered:查找到所需记录占总扫描记录数的比例。 Extra:额外的信息。 2. 通过 Show Profile 分析 SQL 执行性能值得注意的是,MySQL 是在 5.0.37 版本之后才支持 Show Profile 功能的,如果你不太确定的话,可以通过 select @@have_profiling 查询是否支持该功能 Show Profiles 只显示最近发给服务器的 SQL 语句,默认情况下是记录最近已执行的 15 条记录,我们可以重新设置 profiling_history_size 增大该存储记录,最大值为 100。 获取到 Query_ID 之后,我们再通过 Show Profile for Query ID 语句,就能够查看到对应 Query_ID 的 SQL 语句在执行过程中线程的每个状态所消耗的时间了 常用的 SQL 优化分页优化limit是mysql的语法 1select * from table limit [m],n; 其中,m—— [m]为可选,如果填写表示skip步长,即跳过m条。 n——显示条数。指从第m+1条记录开始,取n条记录。 如: 1select * from stu limit 2,4; 即:取stu表中第3至第6条,共4条记录。 1select * from stu limit 5; 即:取stu表中前5条,共5条记录。 利用子查询优化分页查询取第10000条后面的一条数据id: 1select id from table order by id limit 10000,1 从第10001条数据开始,数10条记录返回 1select * from a where id > (select id from table order by id limit 10000,1) limit 20 刚说到为保证主键id连续,要设定记录只能做逻辑删除,但是,假如说如果真的要物理删除,那解决方法的话就只能先取出前offset条数据的id,再去做偏移取数据。 通过 EXPLAIN 分析可知:子查询遍历索引的范围跟上一个查询差不多,而主查询扫描了更多的行数,但执行时间却减少了,只有 0.004s。这就是因为返回行数只有 20 行了,执行效率得到了明显的提升。 优化 SELECT COUNT(*)近似值、增加汇总统计 优化 SELECT *MySQL 常用的存储引擎有 MyISAM 和 InnoDB,其中 InnoDB 在默认创建主键时会创建主键索引,而主键索引属于聚簇索引,即在存储数据时,索引是基于 B + 树构成的,具体的行数据则存储在叶子节点。 而 MyISAM 默认创建的主键索引、二级索引以及 InnoDB 的二级索引都属于非聚簇索引,即在存储数据时,索引是基于 B + 树构成的,而叶子节点存储的是主键值。 假设我们的订单表是基于 InnoDB 存储引擎创建的,且存在 order_no、status 两列组成的组合索引。此时,我们需要根据订单号查询一张订单表的 status,如果我们使用 select * from order where order_no=’xxx’来查询,则先会查询组合索引,通过组合索引获取到主键 ID,再通过主键 ID 去主键索引中获取对应行所有列的值。 如果我们使用 select order_no, status from order where order_no=’xxx’来查询,则只会查询组合索引,通过组合索引获取到对应的 order_no 和 status 的值。如果你对这些索引还不够熟悉,请重点关注之后的第 34 讲,那一讲会详述数据库索引的相关内容。 总结在开发中,我们要尽量写出高性能的 SQL 语句,但也无法避免一些慢 SQL 语句的出现,或因为疏漏,或因为实际生产环境与开发环境有所区别,这些都是诱因。面对这种情况,我们可以打开慢 SQL 配置项,记录下都有哪些 SQL 超过了预期的最大执行时间。首先,我们可以通过以下命令行查询是否开启了记录慢 SQL 的功能,以及最大的执行时间是多少: 12Show variables like 'slow_query%';Show variables like 'long_query_time'; 如果没有开启,我们可以通过以下设置来开启: 123set global slow_query_log='ON'; //开启慢SQL日志set global slow_query_log_file='/var/lib/mysql/test-slow.log';//记录日志地址set global long_query_time=1;//最大执行时间 除此之外,很多数据库连接池中间件也有分析慢 SQL 的功能。总之,我们要在编程中避免低性能的 SQL 操作出现,除了要具备一些常用的 SQL 优化技巧之外,还要充分利用一些 SQL 工具,实现 SQL 性能分析与监控。 思考题假设有一张订单表 order,主要包含了主键订单编码 order_no、订单状态 status、提交时间 create_time 等列,并且创建了 status 列索引和 create_time 列索引。此时通过创建时间降序获取状态为 1 的订单编码,以下是具体实现代码: 1select order_no from order where status =1 order by create_time desc status和create_time单独建索引,在查询时只会遍历status索引对数据进行过滤,不会用到create_time列索引,将符合条件的数据返回到server层,在server对数据通过快排算法进行排序,Extra列会出现file sort;应该利用索引的有序性,在status和create_time列建立联合索引,这样根据status过滤后的数据就是按照create_time排好序的,避免在server层排序。","tags":[{"name":"Mysql","slug":"Mysql","permalink":"http://damon008.github.io/tags/Mysql/"}]},{"title":"如何优化JVM内存分配","date":"2022-08-08T06:48:42.000Z","path":"2022/08/08/jvm/","text":"JVM 内存分配性能问题JVM 内存分配不合理最直接的表现就是频繁的 GC,这会导致上下文切换等性能问题,从而降低系统的吞吐量、增加系统的响应时间。因此,如果你在线上环境或性能测试时,发现频繁的 GC,且是正常的对象创建和回收,这个时候就需要考虑调整 JVM 内存分配了,从而减少 GC 所带来的性能开销。 对象在堆中的生存周期我们知道,在 JVM 内存模型的堆中,堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 区和 Survivor 区,最后 Survivor 由 From Survivor 和 To Survivor 组成。 当我们新建一个对象时,对象会被优先分配到新生代的 Eden 区中,这时虚拟机会给对象定义一个对象年龄计数器(通过参数 -XX:MaxTenuringThreshold 设置)。 同时,也有另外一种情况,当 Eden 空间不足时,虚拟机将会执行一个新生代的垃圾回收(Minor GC)。这时 JVM 会把存活的对象转移到 Survivor 中,并给对象的年龄 +1。对象在 Survivor 中同样也会经历 MinorGC,每经过一次 MinorGC,对象的年龄将会 +1。 当然了,内存空间也是有设置阈值的,可以通过参数 -XX:PetenureSizeThreshold 设置直接被分配到老年代的最大对象,这时如果分配的对象超过了设置的阀值,对象就会直接被分配到老年代,这样做的好处就是可以减少新生代的垃圾回收。 查看 JVM 堆内存分配我们知道了一个对象从创建至回收到堆中的过程,接下来我们再来了解下 JVM 堆内存是如何分配的。在默认不配置 JVM 堆内存大小的情况下,JVM 根据默认值来配置当前内存大小。我们可以通过以下命令来查看堆内存配置的默认值: 12java -XX:+PrintFlagsFinal -version | grep HeapSizejmap -heap 17284 通过命令,我们可以获得在这台机器上启动的 JVM 默认最大堆内存为 1953MB,初始化大小为 124MB。 在 JDK1.7 中,默认情况下年轻代和老年代的比例是 1:2,我们可以通过–XX:NewRatio 重置该配置项。年轻代中的 Eden 和 To Survivor、From Survivor 的比例是 8:1:1,我们可以通过 -XX:SurvivorRatio 重置该配置项。 在 JDK1.7 中如果开启了 -XX:+UseAdaptiveSizePolicy 配置项,JVM 将会动态调整 Java 堆中各个区域的大小以及进入老年代的年龄,–XX:NewRatio 和 -XX:SurvivorRatio 将会失效,而 JDK1.8 是默认开启 -XX:+UseAdaptiveSizePolicy 配置项的。 还有,在 JDK1.8 中,不要随便关闭 UseAdaptiveSizePolicy 配置项,除非你已经对初始化堆内存 / 最大堆内存、年轻代 / 老年代以及 Eden 区 /Survivor 区有非常明确的规划了。否则 JVM 将会分配最小堆内存,年轻代和老年代按照默认比例 1:2 进行分配,年轻代中的 Eden 和 Survivor 则按照默认比例 8:2 进行分配。这个内存分配未必是应用服务的最佳配置,因此可能会给应用服务带来严重的性能问题。 JVM 内存分配的调优过程分析 GC 日志此时我们可以通过 GC 日志查看具体的回收日志。我们可以通过设置 VM 配置参数,将运行期间的 GC 日志 dump 下来,具体配置参数如下: 1-XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/log/heapTest.log 以下是各个配置项的说明: -XX:PrintGCTimeStamps:打印 GC 具体时间; -XX:PrintGCDetails :打印出 GC 详细日志; -Xloggc: path:GC 日志生成路径。 收集到 GC 日志后,我们就可以使用 GCViewer 工具打开它,进而查看到具体的 GC 日志如下: 主页面显示 FullGC 发生了 13 次,右下角显示年轻代和老年代的内存使用率几乎达到了 100%。而 FullGC 会导致 stop-the-world 的发生,从而严重影响到应用服务的性能。此时,我们需要调整堆内存的大小来减少 FullGC 的发生。 参考指标 GC 频率:高频的 FullGC 会给系统带来非常大的性能消耗,虽然 MinorGC 相对 FullGC 来说好了许多,但过多的 MinorGC 仍会给系统带来压力。 内存:这里的内存指的是堆内存大小,堆内存又分为年轻代内存和老年代内存。首先我们要分析堆内存大小是否合适,其实是分析年轻代和老年代的比例是否合适。如果内存不足或分配不均匀,会增加 FullGC,严重的将导致 CPU 持续爆满,影响系统性能。 吞吐量:频繁的 FullGC 将会引起线程的上下文切换,增加系统的性能开销,从而影响每次处理的线程请求,最终导致系统的吞吐量下降。 延时:JVM 的 GC 持续时间也会影响到每次请求的响应时间。 具体调优方法调整堆内存空间减少 FullGC:通过日志分析,堆内存基本被用完了,而且存在大量 FullGC,这意味着我们的堆内存严重不足,这个时候我们需要调大堆内存空间。 1java -jar -Xms4g -Xmx4g heapTest-0.0.1-SNAPSHOT.jar 以下是各个配置项的说明:-Xms:堆初始大小;-Xmx:堆最大值。 调大堆内存之后,我们再来测试下性能情况,发现吞吐量提高了 40% 左右,响应时间也降低了将近 50%。 调整年轻代减少 MinorGC:通过调整堆内存大小,我们已经提升了整体的吞吐量,降低了响应时间。那还有优化空间吗?我们还可以将年轻代设置得大一些,从而减少一些 MinorGC(第 22 讲有通过降低 Minor GC 频率来提高系统性能的详解)。 1java -jar -Xms4g -Xmx4g -Xmn3g heapTest-0.0.1-SNAPSHOT.jar 设置 Eden、Survivor 区比例:在 JVM 中,如果开启 AdaptiveSizePolicy,则每次 GC 后都会重新计算 Eden、From Survivor 和 To Survivor 区的大小,计算依据是 GC 过程中统计的 GC 时间、吞吐量、内存占用量。这个时候 SurvivorRatio 默认设置的比例会失效。 在 JDK1.8 中,默认是开启 AdaptiveSizePolicy 的,我们可以通过 -XX:-UseAdaptiveSizePolicy 关闭该项配置,或显示运行 -XX:SurvivorRatio=8 将 Eden、Survivor 的比例设置为 8:2。大部分新对象都是在 Eden 区创建的,我们可以固定 Eden 区的占用比例,来调优 JVM 的内存分配性能。 小结 现阶段大多数应用使用 JDK 1.8,其默认回收器是 Parallel Scavenge,并且默认开启了 AdaptiveSizePolicy。 AdaptiveSizePolicy 动态调整 Eden、Survivor 区的大小,存在将 Survivor 区调小的可能。当 Survivor 区被调小后,部分 YGC 后存活的对象直接进入老年代。老年代占用量逐渐上升从而触发 FGC,导致较长时间的 STW。 保持使用 UseParallelGC,显式设置 -XX:SurvivorRatio=8。 建议使用 CMS 垃圾回收器,默认关闭 AdaptiveSizePolicy。(-XX:+UseConcMarkSweepGC) 建议在 JVM 参数中加上 -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution,让 GC log 更加详细,方便定位问题。 在大内存(比如8GB及以上)和高QPS的情况下,确保快速响应时间,可以考虑使用G1垃圾回收器进行垃圾回收,G1垃圾回收期可以每次收集部分垃圾来满足小的停顿时间要求。(##-XX:+UseG1GC -Xmx2048m -Xms2048m -XX:+AlwaysPreTouch) 根据应用场景选择合适的垃圾收集器单个CPU的环境,在用户的桌面应用场景中,可用内存一般不大(几十M至一两百M)启动参数: 1-Xms128m -Xmx128m -XX:+UseSerialGC -XX:TargetSurvivorRatio=90 -XX:NewRatio=1 -XX:+DisableExplicitGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:C:\\Users\\cosmo-101\\Desktop\\gc.log 高吞吐为目标,对暂停时间没有特别高的要求Parallel Scavenge收集器,参数说明如下: 1234-XX:+UseParallelOldGC # 新生代和老年代都使用并行收集器-XX:MaxGCPauseMillis # 控制最大垃圾收集停顿时间,大于0的毫秒数-XX:GCTimeRatio # 设置垃圾收集时间占总时间的比率,0<n<100的整数,相当于设置吞吐量大小-XX:+UseAdaptiveSizePolicy # 就不用手工指定一些细节参数,JVM会根据当前系统运行情况收集性能监控信息,动态调整这些参数,提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomiscs) 启动参数如下: 1-Xms2g -Xmx2g -XX:+UseParallelOldGC -XX:MaxGCPauseMillis=200 -XX:GCTimeRatio=99 -XX:+UseAdaptiveSizePolicy -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=512m -XX:+DisableExplicitGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:C:\\Users\\cosmo-101\\Desktop\\gc.log 希望系统停顿时间最短,注重服务的响应速度推荐CMS收集器,参数说明如下: 123456-XX:+UseConcMarkSweepGC # 指定老年代使用CMS收集器,会默认使用ParNew作为新生代收集器-XX:+CMSScavengeBeforeRemark # 在执行CMS Remark阶段前,执行一次Minor GC,以降低STW的时间。通过 Minor GC可以减少新生代对老年代对象的引用,这样可以减少根对象数量,从而降低CMS Remark的工作量.-XX:CMSFullGCsBeforeCompaction设置执行多少次不压缩的Full GC后,来一次压缩整理;为了减少合并整理过程的停顿时间,默认为0;也就是说每次都执行Full GC,不会进行压缩整理。 启动参数如下: 1-Xms2g -Xmx2g -XX:+UseConcMarkSweepGC -XX:TargetSurvivorRatio=90 -XX:NewRatio=1 -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=512m -XX:+CMSScavengeBeforeRemark -XX:CMSFullGCsBeforeCompaction=10 -XX:+DisableExplicitGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:C:\\Users\\cosmo-101\\Desktop\\gc.log 针对具有大内存、多处理器的机器,低GC延迟,堆大小约6GB或更大时推荐G1收集器,参数说明如下: 【注】:使用G1回收器时,G1打破了以往将收集范围固定在新生代或老年代的模式,不需要为各个空间进行单独设置了,G1算法将堆整体划分为若干个区域(Region)。 12345-XX:+UseG1GC # 指定使用G1收集器-XX:MaxGCPauseMillis # 为G1设置暂停时间目标,默认值为200毫秒-XX:G1HeapRegionSize # 设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region-XX:ParallelGCThreads=n # STW期间,并行线程数。建议设置与处理器相同个数,最多为8。如果处理器多于8个,则将n的值设置为处理器的大约5/8。 启动参数如下: 1-Xms8g -Xmx8g -XX:+UseG1GC -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=512m -XX:+DisableExplicitGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:C:\\Users\\cosmo-101\\Desktop\\gc.log 设置合适的参数123456789101112-XX:TargetSurvivorRatio # 默认值50,当survivor区存放的对象超过这个百分百,则对象会向老年代压缩,因此,有助于将对象留在新生代-XX:NewRatio=1 # 老年代与新生代的比例,默认2:1,有助于将对象预留新生代,新生代Minor GC成本远远小于老年代的Full GC-Xmn=70m-XX:MetaspaceSize=256m # 初始元空间大小,默认21MB-XX:MaxMetaspaceSize=256m # 最大元空间,默认是没有限制的,可以根据GC日志打印,如“Full GC (Metadata GC Threshold)”过于频繁,则调整此参数;未设置,虚拟机会自动从21MB初始大小逐渐增长,每次Full GC后自动调整-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log # 保存gc日志,作为优化的依据-XX:+DisableExplicitGC # 禁止System.gc(),免得程序员误调用gc方法影响性能 Jmeter压测聚合报告参数详解12345678910# 1、Label:每个 JMeter 的 element(例如 HTTP Request)都有一个 Name 属性,这里显示的就是 Name 属性的值;# 2、#Samples:表示这次测试中一共发出了多少个请求,如果模拟10个用户,每个用户迭代10次,那么这里显示100;【我的是用户有100,只迭代一次,因此也是100】# 3、Average:平均响应时间——默认情况下是单个 Request 的平均响应时间(ms);# 4、Median:中位数,也就是 50% 用户的响应时间;# 5、90% Line ~ 99% Line:90% ~99%用户的响应时间;# 6、Min:最小响应时间;# 7、Maximum:最大响应时间;# 8、Error%:本次测试中出现的错误率,即 错误的请求的数量/请求的总数;# 9、Throughput:吞吐量——默认情况下表示每秒完成的请求数(Request per Second);# 11、Sent KB/src:每秒从客户端发送的请求的数量。 不同启动参数压测对比1-Xms128m -Xmx128m -XX:+UseSerialGC -XX:TargetSurvivorRatio=90 -XX:NewRatio=1 -XX:+DisableExplicitGC 内存尽用 1-Xms8g -Xmx8g -XX:+UseG1GC -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=512m -XX:+DisableExplicitGC 总结JVM 内存调优通常和 GC 调优是互补的,基于以上调优,我们可以继续对年轻代和堆内存的垃圾回收算法进行调优。这里可以结合上一讲的内容,一起完成 JVM 调优。 虽然分享了一些 JVM 内存分配调优的常用方法,但我还是建议你在进行性能压测后如果没有发现突出的性能瓶颈,就继续使用 JVM 默认参数,起码在大部分的场景下,默认配置已经可以满足我们的需求了。但满足不了也不要慌张,结合今天所学的内容去实践一下,相信你会有新的收获。 思考题以上我们都是基于堆内存分配来优化系统性能的,但在 NIO 的 Socket 通信中,其实还使用到了堆外内存来减少内存拷贝,实现 Socket 通信优化。你知道堆外内存是如何创建和回收的吗? 堆外内存创建有两种方式:1.使用ByteBuffer.allocateDirect()得到一个DirectByteBuffer对象,初始化堆外内存大小,里面会创建Cleaner对象,绑定当前this.DirectByteBuffer的回收,通过put,get传递进去Byte数组,或者序列化对象,Cleaner对象实现一个虚引用(当内存被回收时,会受到一个系统通知)当Full GC的时候,如果DirectByteBuffer标记为垃圾被回收,则Cleaner会收到通知调用clean()方法,回收改堆外内存DirectByteBuffer","tags":[{"name":"Java","slug":"Java","permalink":"http://damon008.github.io/tags/Java/"}]},{"title":"云原生时代微服务架构设计实践","date":"2022-06-11T01:50:01.000Z","path":"2022/06/11/cloudnative-microservice/","text":"前言微服务架构已经火了很多年了,如:Dubbo、Spring Cloud,再到后来的 Spring Cloud Alibaba,但都是仅限于 Java 语言的瓶颈,如何让各种语言之间的微服务更加有效、快速的通讯,这是当前很多企业需要面临的问题,因为一个企业中,不只是基于单纯的某一种语言开发,这就涉及到多语言服务之间的访问。以 Kubernetes(k8s) 为核心的容器技术掀起的云原生浪潮仍在席卷全球,在轰轰烈烈的数字化转型技术变革中,先行者们开始思考新的技术体系究竟能给行业与社会带来什么,以及如何把 DevOps 等先进的开发管理模型带入各行各业,让更多的企业享受到云原生以及 AI、IoT 等前沿技术革新带来的红利。本专栏的创作重点,则是在于讲述在巨多语言的情况下,该如何设计微服务架构,以及云原生时代的微服务的高可用、自动化等等。 微服务架构微服务发展史在微服务到来之前,一切都是单个服务,当然单体应用程序的存在,暴露的缺点也是不少的,主要有: 复杂性高 团队协作开发成本高 扩展性差 部署效率低下 系统很差的高可用性 复杂性,体现在:随着业务的不断迭代,项目的代码量急剧的增多,项目模块也会随着而增加,整个项目就会变成的非常复杂。 开发成本高,体现在:团队开发几十个人在修改代码,然后一起合并到同一地址分支,打包部署,测试阶段只要有一小块功能有问题,就得重新编译打包部署,重新测试,所有相关的开发人员都得参与其中,效率低下,开发成本极高。 扩展性差,体现在:在新增功能业务的时候,代码层面会考虑在不影响现有的业务基础上编写代码,提高了代码的复杂性。 部署效率低,体现在:当单体应用的代码越来越多,依赖的资源越来越多时,应用编译打包、部署测试一次,需要花费的时间越来越多,导致部署效率低下。 高可用差,体现在:由于所有的业务功能最后都部署到同一个文件,一旦某一功能涉及的代码或者资源有问题,那就会影响到整个文件包部署的功能。举个特别鲜明的示例:上世纪八、九十年代,很多的黄页以及延伸到后来的网站中,很多的展示页面与获取数据的后端都是在一个服务模块中。这就造成一个很不好的影响:如果只是修改极小部分的页面展示或图片展示,则需要把整个服务模块进行打包部署,这样会导致时间的严重浪费以及成本的增加。更加糟糕的是,给用户带来非常不好的体验,用户无法理解的是:只是换个网站的某块微小的展示区,导致了整个网站在那一时刻无法正常的访问。当然,也许,对于那个时候互联网的不发达,人们对于这样的体验,已经算是一种幸福的享受了。 由于单体应用具有以上的种种缺点,导致了一个新名词、新概念的诞生——微服务。 其实,从早年间的单体应用,到 2014 年起,得益于以 Docker 为代表的容器化技术的成熟以及 DevOps 文化的兴起,服务化的思想进一步演化,演变为今天我们所熟知的微服务。那么,微服务到底是啥? 微服务,英文名:microservice,百度百科上将其定义为:SOA 架构的一种变体。微服务(或微服务架构)是一种将应用程序构造为一组低耦合的服务。 微服务有着一些鲜明的特点: 功能单一 服务粒度小 服务间独立性强 服务间依赖性弱 服务独立维护 服务独立部署 微服务将原来耦合在一起的复杂业务拆分为单个服务,规避了原本复杂度无止境的积累,每一个微服务专注于单一功能,并通过定义良好的接口清晰表述服务边界。 由于微服务具备独立的运行进程,所以每个微服务可以独立部署。当业务迭代时只需要发布相关服务的迭代即可,降低了测试的工作量同时也降低了服务发布的风险。 在微服务架构下,当某一组件发生故障时,故障会被隔离在单个服务中。如通过限流、熔断等方式降低错误导致的危害,保障核心业务的正常运行。 微服务发展到现在,带有以下标志:高内聚、低耦合,以业务为中心,自治和高可用。 微服务划分的粒度服务的划分,可以从水平的功能划分,也可从垂直的业务划分,粒度的大小,可以根据当前的产品需求来定位,最关键的是要做到:高内聚、低耦合。 如电商系统为例,如下图: 电商中涉及到业务很可能是最多的,商品、库存、订单、促销、支付、会员、购物车、发票、店铺等等,这个是根据业务的不同来进行模块的划分。微服务划分的粒度一定是要有明确性的,不能因为含糊而新增一个服务模块,这样会导致功能接口的可复用性差。一个好的架构设计,肯定是可复用性很强的结构模式。我喜欢这样的一句话:**微服务的边界 (粒度) 是 “决策”, 而不是个 “标准答案”**。即应该将各微服务划分的方式,深度思考,周全的考量各方面的因素下,所作出的一个”最适合”的架构决策,而不是一个人芸亦芸的”标准答案“。 容器化技术什么是容器什么是容器呢?自然界的解释:容器是指用以容纳物料并以壳体为主的基本装置。但今天讲的容器也是一个容纳物质的载体。那计算机所指的容器(Container)到底是什么呢?容器是镜像(Image)的运行时实例。正如从虚拟机模板上启动 VM 一样,用户也同样可以从单个镜像上启动一个或多个容器。虚拟机和容器最大的区别是容器更快并且更轻量级,与虚拟机运行在完整的操作系统之上相比,容器会共享其所在主机的操作系统/内核。 为什么要用容器呢?假设你在使用一台电脑开发一个应用,而且开发环境具有特定的配置。其他开发人员身处的环境配置可能稍有不同。你正在开发的应用不止依赖于您当前的配置,还需要某些特定的库、依赖项和文件。与此同时,你的企业还拥有标准化的开发和生产环境,有着自己的配置和一系列支持文件。你希望尽可能多在本地模拟这些环境,而不产生重新创建服务器环境的开销。这时候,就会需要容器来模拟这些环境。 我们常见的容器启动方式是 Docker,Docker 是一个开源的应用容器引擎,基于 Go 语言 并遵从 Apache2.0 协议开源。Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何 Linux 机器上,也可以实现虚拟化。 KubernetesGoogle 多年来一直使用容器作为交付应用程序的一种重要方式,且运行有一款名为 Borg 的编排工具。Google、RedHat 等公司为了对抗以 Docker 公司为核心的容器商业生态,他们一起成立了 CNCF(Cloud Native Computing Foundation)。当谷歌于 2014 年 3 月开始开发 Kubernetes 时,很明智的选择当时最流行的容器,没错,就是 Docker。Kubernetes 对 Docker 容器运行时的支持,迎来了大量的使用用户。Kubernetes 于 2014 年 6 月 6 日首次发布。这便有了容器编排工具 Kubernetes 的诞生。另外,CNCF 的目的是以开源的 K8S 为基础,使得 K8S 能够在容器编排方面能够覆盖更多的场景,提供更强的能力。K8S 必须面临 Swarm 和 Mesos 的挑战。Swarm 的强项是和 Docker 生态的天然无缝集成,Mesos 的强项是大规模集群的管理和调度。K8S 是 Google 基于公司已经使用了十多年的 Borg 项目进行了沉淀和升华才提出的一套框架。它的优点就是有一套完整的全新的设计理念,同时有 Google 的背书,而且在设计上有很强的扩展性,所以,最终 K8S 赢得了胜利,成为了容器生态的行业标准。 K8s 为什么会成为微服务的基础架构为什么 K8s 是下一代微服务架构基础微服务出现后,同样面临着一个重要的话题:高可用。所谓高可用:英文缩写 HA(High Availability),是指当某个服务或服务所在节点出现故障时,其对外的功能可以转移到该服务其他的副本或该服务在其他节点的副本,从而在减少停工时间的前提下,满足业务的持续性,这两个或多个服务构成了服务高可用。同时,这种高可用需要考虑到服务的性能压力,即服务的负载均衡。 我们知道对于服务的高可用,或者说服务的负载来说,有很多方式来解决这些问题。比如: 主从方式,其工作原理是:主机处于工作状态,备机处于监控、准备状态,当主机出现宕机的情况下,备机可以接管主机的一切工作,等到主机恢复正常后,将会手动或自动的方式将服务切换到主机上运行,数据的一致性通过共享存储来实现。 集群方式,其工作原理是:多台机器同时运行一个或几个服务,当其中的某个节点出现宕机时,这时该节点的服务将无法提供业务功能,可以选择根据一定的机制,将服务请求转移到该服务所在的其他节点上,这样可以让逻辑持续的执行下去,即消除软件单点故障。这其实就涉及到负载均衡策略。 对于微服务的高可用,涉及到的其中一个就是其服务的负载均衡。在微服务中,负载均衡的前提是,同一个服务需要被发现多个,或者说多个副本,这样才能实现负载均衡以及服务的高可用。 同时,服务发现后,其实面临的是一个主要的问题就是应该访问哪一个?因为发现了某个服务的多个实例,最终只会访问其中某一个,这就涉及到服务的负载均衡了。 负载均衡在微服务中是一个很常见的话题,实现负载均衡的插件也越来越多。netflix 开源的 Zuul、Gateway 等等。 但这样的微服务,带来的好处就是高度自治,但同时也会带来一定的副作用:所用到的技术栈太过复杂,整个系统看起来很繁重。 K8s 是如何解决这些问题的呢?在 K8s 中提供了一套服务注册与发现的机制:Kubernetes 为服务和 Pod 创建 DNS 记录。您可以使用一致的 DNS 名称而不是 IP 地址联系服务。集群中定义的每个服务(包括 DNS 服务器本身)都分配了一个 DNS 名称。默认情况下,客户端 Pod 的 DNS 搜索列表包括 Pod 自己的命名空间和集群的默认域。DNS 查询可以使用 pod 的 /etc/resolv.conf. Kubelet 为每个 pod 设置这个文件。例如,对查询 data 可以扩展为 test.default.svc.cluster.local。该 search 选项的值用于扩展查询: 1234567891011apiVersion: v1kind: Servicemetadata: name: testspec: selector: app: MyApp ports: - protocol: TCP port: 80 targetPort: 9376 该规范创建了一个名为“test”的新服务对象,其目标是任何带有 app=MyApp 标签的 Pod 上的 TCP 端口 9376 。 同时,K8s 提供一种资源 Configmap,可以编写一个 spec 引用 ConfigMap 的 Pod ,并根据 ConfigMap 中的数据配置该 Pod 中的容器。Pod 和 ConfigMap 必须在同一个命名空间: 1234567891011121314kind: ConfigMapapiVersion: v1metadata: name: rest-service namespace: system-serverdata: application.yaml: |- greeting: message: Say Hello to the World --- spring: profiles: dev greeting: message: Say Hello to the Developers 再者,对于服务的暴露,K8s 提供了一种资源:Ingress controller,Ingress 控制器类似 Nginx,可以帮助我们把服务代理到集群外,提供给前端或外界第三方使用。 这样,对于系统本身的复杂程度,可以摒弃使用 Spring cloud 自带的各种组件: K8s 的基础以及实战K8s 基础在前面,我们讲述了 K8s 为什么可以替换 Springcloud 家族中的组件来统一管理服务、访问服务。接下来,我们讲讲 K8s 都有哪些基础常用的资源。这些资源在 K8s 中有其接口功能,但这里,我们统一用脚本命令的形式来调用接口,生成资源。 首先第一条就是编写配置文件,因为配置文件可以是 YAML 或者 JSON 格式的。为方便阅读与理解,在后面的讲解中,我会统一使用 YAML 文件来指代它们。 Kubernetes 跟 Docker 等很多项目最大的不同,就在于它不推荐你使用命令行的方式直接运行容器(虽然 Kubernetes 项目也支持这种方式,比如:kubectl run),而是希望你用 YAML 文件的方式,即:把容器的定义、参数、配置,统统记录在一个 YAML 文件中,然后用这样一句指令把它运行起来: 1kubectl create -f xxx.yaml 这样做最直接的一个好处是:你会有一个文件能记录下 K8s 到底 run 了哪些东西。比如下面这个例子: 12345678910111213141516171819apiVersion: apps/v1kind: Deploymentmetadata: name: tomcat-deploymentspec: selector: matchLabels: app: tomcat replicas: 2 template: metadata: labels: app: tomcat spec: containers: - name: tomcat image: tomcat:10.0.5 ports: - containerPort: 80 像这样的一个 YAML 文件,对应到 kubernetes 中,就是一个 API Object(API 对象)。当你为这个对象的各个字段填好值并提交给 Kubernetes 之后,Kubernetes 就会负责创建出这些对象所定义的容器或者其他类型的 API 资源。可以看到,这个 YAML 文件中的 Kind 字段,指定了这个 API 对象的类型(Type),是一个 Deployment。Deployment 是一个定义多副本应用(即多个副本 Pod)的对象。此外,Deployment 还负责在 Pod 定义发生变化时,对每个副本进行滚动更新(Rolling+Update)。 在上面这个 Yaml 文件中,我给它定义的 Pod 副本个数 (spec.replicas)是:2。但,这些 Pod 副本长啥样子呢?为此,我们定义了一个 Pod 模版(spec.template),这个模版描述了我想要创建的 Pod 的细节。在上面的例子里,这个 Pod 里只有一个容器,这个容器的镜像(spec.containers.image)是 tomcat=10.0.5,这个容器监听端口(containerPort)是 80。 需要注意的是,像这种,使用一种 API 对象(Deployment)管理另一种 API 对象(Pod)的方法,在 Kubernetes 中,叫作“控制器”模式(controller pattern)。在我们的这个 demo 中,Deployment 扮演的正是 Pod 的控制器的角色。而 Pod 是 Kubernetes 世界里的应用;而一个应用,可以由多个容器(container)组成。为了让我们这个 tomcat 服务容器化运行起来,我们只需要执行: 12tom@PK001:~/damon$ kubectl create -f tomcat-deployment.yamldeployment.apps/tomcat-deployment created 执行完上面的命令后,你就可以看容器运行情况,此时,只需要执行: 1234tom@PK001:~/damon$ kubectl get pod -l app=tomcatNAME READY STATUS RESTARTS AGEtomcat-deployment-799f46f546-7nxrj 1/1 Running 0 77stomcat-deployment-799f46f546-hp874 0/1 Running 0 77s kubectl get 指令的作用,就是从 Kubernetes 里面获取(GET)指定的 API 对象。可以看到,在这里我还加上了一个 -l 参数,即获取所有匹配 app=nginx 标签的 Pod。需要注意的是,在命令行中,所有 key-value 格式的参数,都使用“=”而非“:”表示。 从这条指令返回的结果中,我们可以看到现在有两个 Pod 处于 Running 状态,也就意味着我们这个 Deployment 所管理的 Pod 都处于预期的状态。 此外, 你还可以使用 kubectl describe 命令,查看一个 API 对象的细节,比如: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849tom@PK001:~/damon$ kubectl describe pod tomcat-deployment-799f46f546-7nxrjName: tomcat-deployment-799f46f546-7nxrjNamespace: defaultPriority: 0Node: ca005/10.10.2.5Start Time: Thu, 08 Apr 2021 10:41:08 +0800Labels: app=tomcat pod-template-hash=799f46f546Annotations: cni.projectcalico.org/podIP: 20.162.35.234/32Status: RunningIP: 20.162.35.234Controlled By: ReplicaSet/tomcat-deployment-799f46f546Containers: tomcat: Container ID: docker://5a734248525617e950b7ce03ad7a19acd4ffbd71c67aacd9e3ec829d051b46d3 Image: tomcat:10.0.5 Image ID: docker-pullable://tomcat@sha256:2637c2c75e488fb3480492ff9b3d1948415151ea9c503a496c243ceb1800cbe4 Port: 80/TCP Host Port: 0/TCP State: Running Started: Thu, 08 Apr 2021 10:41:58 +0800 Ready: True Restart Count: 0 Environment: <none> Mounts: /var/run/secrets/kubernetes.io/serviceaccount from default-token-2ww52 (ro)Conditions: Type Status Initialized True Ready True ContainersReady True PodScheduled TrueVolumes: default-token-2ww52: Type: Secret (a volume populated by a Secret) SecretName: default-token-2ww52 Optional: falseQoS Class: BestEffortNode-Selectors: <none>Tolerations: node.kubernetes.io/not-ready:NoExecute for 300s node.kubernetes.io/unreachable:NoExecute for 300sEvents: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 4m17s default-scheduler Successfully assigned default/tomcat-deployment-799f46f546-7nxrj to ca005 Normal Pulling 4m16s kubelet, ca005 Pulling image \"tomcat:10.0.5\" Normal Pulled 3m27s kubelet, ca005 Successfully pulled image \"tomcat:10.0.5\" Normal Created 3m27s kubelet, ca005 Created container tomcat Normal Started 3m27s kubelet, ca005 Started container tomcat 在 kubectl describe 命令返回的结果中,可以的清楚地看到这个 Pod 的详细信息,比如它的 IP 地址等等。其中,有一个部分值得你特别关注,它就是 Events(事件)。 在 Kubernetes 执行的过程中,对 API 对象的所有重要操作,都会被记录在这个对象的 Events 里,并且显示在 kubectl describe 指令返回的结果中。这些 Events 中的信息很重要,可以排查容器是否运行、正常运行的原因。 如果你希望升级 tomcat 的版本,那可以直接修改 Yaml 文件: 123456spec: containers: - name: tomcat image: tomcat:latest ports: - containerPort: 80 修改完 Yaml 文件后,执行: 1kubectl apply -f tomcat-deployment.yaml 这样的操作方法,是 Kubernetes“声明式 API”所推荐的使用方法。也就是说,作为用户,你不必关心当前的操作是创建,还是更新,你执行的命令始终是 kubectl apply,而 Kubernetes 则会根据 YAML 文件的内容变化,自动进行具体的处理。 同时,可以查看容器内的服务的日志情况: 123456789101112131415161718192021222324252627282930313233343536tom@PK001:~/damon$ kubectl logs -f tomcat-deployment-799f46f546-7nxrjNOTE: Picked up JDK_JAVA_OPTIONS: --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED08-Apr-2021 02:41:59.037 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version name: Apache Tomcat/10.0.508-Apr-2021 02:41:59.040 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server built: Mar 30 2021 08:19:50 UTC08-Apr-2021 02:41:59.040 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version number: 10.0.5.008-Apr-2021 02:41:59.040 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Name: Linux08-Apr-2021 02:41:59.040 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Version: 4.4.0-116-generic08-Apr-2021 02:41:59.040 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Architecture: amd6408-Apr-2021 02:41:59.040 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Java Home: /usr/local/openjdk-1108-Apr-2021 02:41:59.040 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Version: 11.0.10+908-Apr-2021 02:41:59.040 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Vendor: Oracle Corporation08-Apr-2021 02:41:59.040 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log CATALINA_BASE: /usr/local/tomcat08-Apr-2021 02:41:59.041 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log CATALINA_HOME: /usr/local/tomcat08-Apr-2021 02:41:59.051 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.lang=ALL-UNNAMED08-Apr-2021 02:41:59.051 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.io=ALL-UNNAMED08-Apr-2021 02:41:59.051 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.util=ALL-UNNAMED08-Apr-2021 02:41:59.051 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.util.concurrent=ALL-UNNAMED08-Apr-2021 02:41:59.052 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED08-Apr-2021 02:41:59.052 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties08-Apr-2021 02:41:59.052 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager08-Apr-2021 02:41:59.052 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djdk.tls.ephemeralDHKeySize=204808-Apr-2021 02:41:59.052 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.protocol.handler.pkgs=org.apache.catalina.webresources08-Apr-2021 02:41:59.052 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dorg.apache.catalina.security.SecurityListener.UMASK=002708-Apr-2021 02:41:59.052 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dignore.endorsed.dirs=08-Apr-2021 02:41:59.052 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dcatalina.base=/usr/local/tomcat08-Apr-2021 02:41:59.052 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dcatalina.home=/usr/local/tomcat08-Apr-2021 02:41:59.052 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.io.tmpdir=/usr/local/tomcat/temp08-Apr-2021 02:41:59.056 INFO [main] org.apache.catalina.core.AprLifecycleListener.lifecycleEvent Loaded Apache Tomcat Native library [1.2.27] using APR version [1.6.5].08-Apr-2021 02:41:59.056 INFO [main] org.apache.catalina.core.AprLifecycleListener.lifecycleEvent APR capabilities: IPv6 [true], sendfile [true], accept filters [false], random [true], UDS [true].08-Apr-2021 02:41:59.059 INFO [main] org.apache.catalina.core.AprLifecycleListener.initializeSSL OpenSSL successfully initialized [OpenSSL 1.1.1d 10 Sep 2019]08-Apr-2021 02:41:59.312 INFO [main] org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler [\"http-nio-8080\"]08-Apr-2021 02:41:59.331 INFO [main] org.apache.catalina.startup.Catalina.load Server initialization in [441] milliseconds08-Apr-2021 02:41:59.369 INFO [main] org.apache.catalina.core.StandardService.startInternal Starting service [Catalina]08-Apr-2021 02:41:59.370 INFO [main] org.apache.catalina.core.StandardEngine.startInternal Starting Servlet engine: [Apache Tomcat/10.0.5]08-Apr-2021 02:41:59.377 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler [\"http-nio-8080\"]08-Apr-2021 02:41:59.392 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [61] milliseconds 在这里,为什么会以 Deployment 资源来举例呢?因为在 K8s 资源中,Deployment 形式的资源提供了声明更新以及副本集,可以在 Deployment 中描述了“所需的状态”,并且 Deployment 以受控速率将实际状态更改为所需状态。您可以定义部署以创建新的副本集,或删除现有部署并通过新部署采用其所有资源。在 rc 滚动升级时,为了防止服务访问的中断,引入了 Deployment 资源。 接下来,我们看看 K8s 比较重要的资源 ConfigMap,其是为 Pod 的配置信息起作用,通过服务挂载的形式来提供各种配置: 12345678910111213141516171819202122232425kind: ConfigMapapiVersion: v1metadata: name: rest-service namespace: system-serverdata: application.yaml: |- greeting: message: Say Hello to the World --- spring: profiles: dev greeting: message: Say Hello to the Developers --- spring: profiles: test greeting: message: Say Hello to the Test --- spring: profiles: prod greeting: message: Say Hello to the Prod 当然,它支持各种形式的挂载,key-value 字符串、文件形式等。这在微服务中解耦合,非常重要,比如:在一次线上环境中,部署的服务可能需要对其某个或某几个参数进行修改,此时,如果之前编码时,将这些参数解耦到配置资源中,则可以通过修改配置来动态刷新服务配置: 1kubectl edit cm rest-service -n system-server 在执行这个命令编辑这个服务的配置后,我们可以看到服务的日志信息: 12345672021-11-29 07:59:52.860:152 [OkHttp https://10.16.0.1/...] INFO org.springframework.cloud.kubernetes.config.reload.EventBasedConfigurationChangeDetector -Detected change in config maps2021-11-29 07:59:52.862:74 [OkHttp https://10.16.0.1/...] INFO org.springframework.cloud.kubernetes.config.reload.EventBasedConfigurationChangeDetector -Reloading using strategy: REFRESH2021-11-29 07:59:53.444:112 [OkHttp https://10.16.0.1/...] INFO org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration -Located property source: [BootstrapPropertySource {name='bootstrapProperties-configmap.rest-service.system-server'}]2021-11-29 07:59:53.499:652 [OkHttp https://10.16.0.1/...] INFO org.springframework.boot.SpringApplication -The following profiles are active: kubernetes,dev2021-11-29 07:59:53.517:652 [OkHttp https://10.16.0.1/...] INFO org.springframework.boot.SpringApplication -The following profiles are active: kubernetes,dev2021-11-29 07:59:53.546:61 [OkHttp https://10.16.0.1/...] INFO org.springframework.boot.SpringApplication -Started application in 0.677 seconds (JVM running for 968605.422)2021-11-29 07:59:53.553:61 [OkHttp https://10.16.0.1/...] INFO org.springframework.boot.SpringApplication -Started application in 0.685 seconds (JVM running for 968617.369) 日志中 Detected change in config maps、Reloading using strategy: REFRESH,表示通过修改配置后达到了自动刷新的效果。 接下来,我们再看看服务的注册与发现,如果单纯地从 K8s 原生,那其提供了一种域名访问形式来进行服务间的相互调用:$(service name).$(namespace).svc.cluster.local,其中 cluster.local 为指定的集群的域名,这里表示本地集群。 同时,Service 既然是定义一个服务的多种 pod 的逻辑合集以及一种访问 pod 的策略。 Service 的类型有四种: ExternalName:创建一个 DNS 别名指向 service name,这样可以防止 service name 发生变化,但需要配合 DNS 插件使用。 ClusterIP:默认的类型,用于为集群内 Pod 访问时,提供的固定访问地址,默认是自动分配地址,可使用 ClusterIP 关键字指定固定 IP。 NodePort:基于 ClusterIp,用于为集群外部访问 Service 后面 Pod 提供访问接入端口。 LoadBalancer:它是基于 NodePort。 从上面讲的 Service,我们可以看到一种场景:所有的微服务在一个局域网内,或者说在一个 K8s 集群下,那么可以通过 Service 用于集群内 Pod 的访问,这就是 Service 默认的一种类型 ClusterIP,ClusterIP 这种的默认会自动分配地址。 那么问题来了,既然可以通过上面的 ClusterIp 来实现集群内部的服务访问,那么如何注册服务呢?其实 K8s 并没有引入任何的注册中心,使用的就是 K8s 的 kube-dns 组件。然后 K8s 将 Service 的名称当做域名注册到 kube-dns 中,每一个 Service 在 kube-dns 中都有一条 DNS 记录,同时,如果有服务的 ip 更换,kube-dns 自动会同步,对服务来说是不需要改动的。通过 Service 的名称就可以访问其提供的服务。那么问题又来了,如果一个服务的 pod 对应有多个,那么如何实现 LB?其实,最终通过 kube-proxy,实现负载均衡。也就是说 kube-dns 通过 servicename 找到指定 clusterIP,kube-proxy 完成通过 clusterIP 到 PodIP 的过程。 说到这,我们来看下 Service 的服务发现与负载均衡的策略,Service 负载分发策略有两种: RoundRobin:轮询模式,即轮询将请求转发到后端的各个 pod 上,其为默认模式。 SessionAffinity:基于客户端 IP 地址进行会话保持的模式,类似 IP Hash 的方式,来实现服务的负载均衡。 但这种原生提供的服务访问形式还是带有一点遗憾:就是需要带有 Service 的所在命名空间,这也许 K8s 有其自身的考虑,假如我这里有一个 Service: 12345678910111213apiVersion: v1kind: Servicemetadata: name: rest-service-service namespace: system-serverspec: type: NodePort ports: - name: rest-svc port: 2001 targetPort: 2001 selector: app: rest-service 这个 Service 表示目标是监视 http 协议端口为 2001 的服务的一组 pod,这样,但访问该 Service 时,会通过其域名进行解析到 pod 的信息来访问 pod 的 IP 和 port: 12system-server rest-service-deployment-cc7c5b559-6t4lp 1/1 Running 6 11d 10.244.0.188 leinao-deploy-server <none> <none>system-server rest-service-deployment-cc7c5b559-gpg4m 1/1 Running 6 11d 10.244.0.189 leinao-deploy-server <none> <none> 这样,当我们在容器内通过 rest-service-service.system-server.svc.cluster.local:2001/api 访问服务时,这样,我们可以看到默认的类型是 ClusterIP,用于为集群内 Pod 访问时,可以先通过域名来解析到 2 个服务地址信息,然后再通过 LB 策略来选择其中一个作为请求的对象。 好了,以上就是常见的几种 K8s 资源,当然,还有更多的资源(DaemonSet、StatefulSet、ReplicaSet 等)感兴趣可以参见官网。 实战 K8s 下微服务的架构实现在《Spring Boot 2.x 结合 k8s 实现分布式微服务架构》 Chat 中,我们简单讲述了如何结合 K8s 来实现分布式微服务的架构。 但这里我们遗留了几个问题: Oauth2 高可用的实现 如何实现跨命名空间的服务的访问 如何实现分布式服务的灰度、蓝绿发布 针对以上几点问题,我们来一一破解。 Oauth2 的高可用实现我们知道,对于 Oauth2 原生中,提供了两种方式来进行服务的鉴权: 获取用户信息来进行鉴权 通过检验 token 来进行鉴权 1234567891011121314151617security: path: ignores: /,/index,/static/**,/css/**, /image/**, /favicon.ico, /js/**,/plugin/**,/avue.min.js,/img/**,/fonts/** oauth2: client: client-id: rest-service client-secret: rest-service-123 user-authorization-uri: ${cas-server-url}/oauth/authorize access-token-uri: ${cas-server-url}/oauth/token resource: loadBalanced: true id: rest-service prefer-token-info: true token-info-uri: ${cas-server-url}/oauth/check_token #user-info-uri: ${cas-server-url}/api/v1/user authorization: check-token-access: ${cas-server-url}/oauth/check_token 配置中,user-info-uri、token-info-uri 就是用来进行服务客户端的鉴权,但不能同时存在,但对于原生的user-info-uri,并没有提供合理的鉴权逻辑,可能存在一些问题:当用户登录后,发现所有的接口都可以正常访问,无论是需要权限的,或者是不需要权限的,存在一定的问题坑。 这里,我们就不再使用获取用户信息方式来进行鉴权、授权。我们来看看check_token这种方式是如何进行鉴权授权的呢? 原来,当用户携带 token 请求资源服务器的资源时, OAuth2AuthenticationProcessingFilter 会拦截 token: 最后会进入 loadAuthentication 去进行 token 的检验过程: 至于校验 Token 的处理逻辑很简单,就是调用 redisTokenStore 查询 token 的合法性,及其返回用户的部分信息: 最后如果 ok 的话,返回给在这里 RemoteTokenServices,最重要的是 **userTokenConverter.extractAuthentication(map)**,判断是否有 userDetailsService 实现,如果有的话去根据返回的信息查询一次全部的用户信息,没有实现直接返回 username: 基于此,进行 token 和 userdetails 过程,把无状态的 token 转化成用户信息。 这样其实还是服务间的互相调用,要保证这种调用的高可用,无非就是服务的多节点、Redis 的高可用。当然如果你是采用 Redis 的话,如果是 JWT 模式,那就更简单了,直接无状态形式存储 Token。我们可以把统一认证中心做成 K8s 中的 Deployment 类型: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849apiVersion: apps/v1kind: Deploymentmetadata: name: cas-server-deployment namespace: system-server labels: app: cas-serverspec: replicas: 3 selector: matchLabels: app: cas-server template: metadata: labels: app: cas-server spec: nodeSelector: cas-server: \"true\" containers: - name: cas-server image: {{ cluster_cfg['cluster']['docker-registry']['prefix'] }}cas-server imagePullPolicy: Always ports: - name: cas-server01 containerPort: 2000 volumeMounts: - mountPath: /home/cas-server name: cas-server-path - mountPath: /data/cas-server name: cas-server-log-path - mountPath: /etc/kubernetes name: kube-config-path - mountPath: /abnormal_data_dir name: abnormal-data-dir args: [\"sh\", \"-c\", \"nohup java $JAVA_OPTS -jar -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m -Xms1024m -Xmx1024m -Xmn256m -Xss256k -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC cas-server.jar --spring.profiles.active=dev\", \"&\"] volumes: - name: cas-server-path hostPath: path: /var/pai/cas-server - name: cas-server-log-path hostPath: path: /data/cas-server - name: kube-config-path hostPath: path: /etc/kubernetes - name: abnormal-data-dir hostPath: path: /data/images/detect_result/defect 在这里,我们定义了一个名称为 cas-server-deployment 的资源,同时,我们定义了在创建它的时候,会创建三个副本:replicas: 3,这样来保证 cas-server 的高可用。同时,我们为了它更好的被发现,我们利用 Service 资源来进行服务的负载均衡: 123456789101112apiVersion: v1kind: Servicemetadata: name: cas-server-service namespace: system-serverspec: ports: - name: cas-server01 port: 2000 targetPort: cas-server01 selector: app: cas-server 这里定义的是一个目标为 http 协议,端口为 2000 的 pod 的副本集的资源,这个默认是 ClusterIP 模式的 Service,通过 Service 直接在集群内部进行访问:cas-server-service.system-server.svc.cluster.local:2000/api。这样,利用 K8s 的 Service 来实现服务注册与发现。同时,结合 Deployment 资源进行服务多节点的部署,我们就可以实现服务的高可用。 如何实现跨命名空间的服务的访问在 K8s 中,前面讲过,只能通过命名空间的访问方式来请求其它 namespace 下的服务,对于原生 K8s 的服务调用是这样的,但是,我们基于 spring-cloud,这里可以对其进行改造。我们引入 spring-cloud-k8s 后,摒弃 基于 Ribbon 的负载均衡,我们采用基于 spring-cloud-loadbalancer 的策略来进行尝试: 123456789101112spring: application: name: cas-server cloud: loadbalancer: ribbon: enabled: false kubernetes: ribbon: mode: SERVICE discovery: all-namespaces: true 这里有个配置:spring.cloud.kubernetes.ribbon.mode=SERVICE,这个是干嘛的呢?其实际是禁用了 Ribbon 的 LB 能力,此时不会生效,走的还是 Spring cloud LoadBalancer。另外对于 Service,这里都设置为 NodePort 类型,如果是默认类型是否可以实现 LB,需要待确认,因为目前来看,没有实现,可能是网络问题,并不是说默认类型的 Service 不可实现 LB。同时,我们还是需要配置:spring.cloud.loadbalancer.ribbon.enabled = false,因为这个默认是 true 的。 当然,这里既然弃用 Ribbon,那需要引入依赖: 1234<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-kubernetes-loadbalancer</artifactId></dependency> 这样,当我们在我们进行资源服务器访问的时候,资源服务器会调用统一认证中心进行 token 的校验,此时,就可以通过 http://cas-server-service/oauth/check_token 来进行检验,这样就实现了服务的高可用。同时,即使资源服务器和统一认证中心不在同一个 namespace,也可以通过该种方式来进行请求访问。具体原理是,其会获取 K8s 集群中所有可被发现的 Service,这样对于不同 namespace 下的 Service 也就存在可以被互调的可能。 如这里在通过访问 A 命名空间下的服务时,通过 Serice 访问后看到日志: 这就说明,可以通过 Service 方式实现跨命名空间的服务互调。 如何实现分布式服务的灰度、蓝绿发布在云原生最佳实践中,涵盖了灰度发布、弹性伸缩、集群迁移、网络通信、应用容器化改造等等场景,今天我们就来利用 K8s 原生技术来实现分布式微服务的灰度发布以及蓝绿发布。 通常使用无状态负载 Deployment、有状态负载 StatefulSet 等 Kubernetes 对象来部署业务,每个工作负载管理一组 Pod。以 Deployment 为例,示意图如下: 我们为这个工作服务来创建 Service,Service 通过 selector 来选择服务节点 Pod,接下来,我们进行灰度发布该工作应用负载。 灰度发布通常会为每个 Deployment 类型的应用负载创建一个 Service,但 K8s 并未限制 Service 需与 Deployment 负载是一一对应关系。Service 只通过 selector 匹配负载节点 Pod,若不同 Deployment 的负载节点 Pod 被同一 selector 选中,即可实现一个 Service 对应多个版本 Deployment。调整不同版本 Deployment 的副本数,即可调整不同版本服务的权重,来实现灰度发布。 那么,既然了解到灰度发布的原理,我们来进行实战,假如这里有一个服务提供者 diss-ns-service,此时我们可以对其进行创建不同版本的 Deployment 类型负载 Pod: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051apiVersion: apps/v1kind: Deploymentmetadata: name: diff-ns-service-v1-deployment namespace: ns-app labels: app: diff-ns-servicespec: replicas: 3 selector: matchLabels: app: diff-ns-service version: v1 template: metadata: labels: app: diff-ns-service version: v1 spec: nodeSelector: diff-ns-service: \"true\" containers: - name: diff-ns-service image: diff-ns-service:v1 imagePullPolicy: Always ports: - name: diff-ns containerPort: 2001 volumeMounts: - mountPath: /home/diff-ns-service name: diff-ns-service-path - mountPath: /data/diff-ns-service name: diff-ns-service-log-path - mountPath: /etc/kubernetes name: kube-config-path - mountPath: /abnormal_data_dir name: abnormal-data-dir args: [\"sh\", \"-c\", \"nohup java $JAVA_OPTS -jar -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m -Xms1024m -Xmx1024m -Xmn256m -Xss256k -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC diff-ns-service.jar --spring.profiles.active=dev\", \"&\"] volumes: - name: diff-ns-service-path hostPath: path: /var/pai/diff-ns-service - name: diff-ns-service-log-path hostPath: path: /data/diff-ns-service - name: kube-config-path hostPath: path: /etc/kubernetes - name: abnormal-data-dir hostPath: path: /data/images/detect_result/defect 以上是 v1 版本的应用负载,接下来 v2 版本也是一样的: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051apiVersion: apps/v1kind: Deploymentmetadata: name: diff-ns-service-v2-deployment namespace: ns-app labels: app: diff-ns-servicespec: replicas: 3 selector: matchLabels: app: diff-ns-service version: v2 template: metadata: labels: app: diff-ns-service version: v2 spec: nodeSelector: diff-ns-service: \"true\" containers: - name: diff-ns-service image: diff-ns-service:v2 imagePullPolicy: Always ports: - name: diff-ns containerPort: 2001 volumeMounts: - mountPath: /home/diff-ns-service name: diff-ns-service-path - mountPath: /data/diff-ns-service name: diff-ns-service-log-path - mountPath: /etc/kubernetes name: kube-config-path - mountPath: /abnormal_data_dir name: abnormal-data-dir args: [\"sh\", \"-c\", \"nohup java $JAVA_OPTS -jar -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m -Xms1024m -Xmx1024m -Xmn256m -Xss256k -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC diff-ns-service.jar --spring.profiles.active=dev\", \"&\"] volumes: - name: diff-ns-service-path hostPath: path: /var/pai/diff-ns-service - name: diff-ns-service-log-path hostPath: path: /data/diff-ns-service - name: kube-config-path hostPath: path: /etc/kubernetes - name: abnormal-data-dir hostPath: path: /data/images/detect_result/defect 同时,这里我们对负载的副本数都设置为 3,表示 3 个负载节点 pod,此时,我们创建完之后可以看到: 1234567ns-app diff-ns-service-v1-deployment-d88b9c4fd-22lgb 1/1 Running 0 12sns-app diff-ns-service-v1-deployment-d88b9c4fd-cgsqw 1/1 Running 0 12sns-app diff-ns-service-v1-deployment-d88b9c4fd-hmcbq 1/1 Running 0 12sns-app diff-ns-service-v2-deployment-37bf53d4b-43w23 1/1 Running 0 12sns-app diff-ns-service-v2-deployment-37bf53d4b-ce33g 1/1 Running 0 12sns-app diff-ns-service-v2-deployment-37bf53d4b-scds6 1/1 Running 0 12s 这样,我们对负载 diff-ns-service 创建了不同版本的资源 Pod,接下来,我们创建一个 Service,这个是这样的 YAML: 123456789101112apiVersion: v1kind: Servicemetadata: name: diff-ns-service-service namespace: ns-appspec: ports: - name: diff-ns-svc port: 2001 targetPort: 2001 selector: app: diff-ns-service 我们可以看到在 selector 中不指定版本,这样,可以让 Service 同时选中两个版本的 Deployment 的 Pod。此时,我们通过脚本命令来执行访问: 1for i in {1..10}; do curl http://diff-ns-service-service/getservicedetail?servicename=aaa; done; 我们来看看打印的日志: 12345678910diff-ns-service-v1diff-ns-service-v2diff-ns-service-v1diff-ns-service-v2diff-ns-service-v1diff-ns-service-v2diff-ns-service-v1diff-ns-service-v2diff-ns-service-v1diff-ns-service-v2 可以看到返回结果一半为 v1 版本的响应,一半为 v2 版本的响应。 接下来,我们通过 kubectl 方式修改负载的副本数: 123kubectl scale deployment/diff-ns-service-v2-deployment --replicas=4kubectl scale deployment/diff-ns-service-v1-deployment --replicas=1 因为我们需要作版本的更新,所以把新版本 v2 设置为 4,旧版本 v1 的设置为 1,接下来,我们继续通过 curl 命令来测试: 12345678910diff-ns-service-v2diff-ns-service-v2diff-ns-service-v1diff-ns-service-v2diff-ns-service-v1diff-ns-service-v2diff-ns-service-v2diff-ns-service-v2diff-ns-service-v2diff-ns-service-v2 我们从结果可以发现,10 次请求访问中,只有 2 次访问的是 v1 的旧版本,v1 与 v2 版本的响应比例与其副本数比例一致,为 4:1。通过控制不同版本服务的副本数就实现了灰度发布。 蓝绿发布接下来看看蓝绿发布,蓝绿发布的原理与灰度发布稍微不同,集群中已部署两个不同版本的 Deployment,其负载 Pod 拥有共同的 label。但有一个 label 值不同,用于区分不同的版本。Service 使用 selector 选中了其中一个版本的 Deployment 的 Pod,此时通过修改 Service 的 selector 中决定服务版本的 label 的值来改变 Service 后端对应的 Pod,即可实现让服务从一个版本直接切换到另一个版本。 所以从原理上看,我们创建的 Service 除了包括本身的一些信息,还需要包括版本信息: 12345678910111213apiVersion: v1kind: Servicemetadata: name: diff-ns-service-service namespace: ns-appspec: ports: - name: diff-ns-svc port: 2001 targetPort: 2001 selector: app: diff-ns-service version: v1 同样,执行以下命令,测试访问。 1for i in {1..10}; do curl http://diff-ns-service-service/getservicedetail?servicename=aaa; done; 返回结果如下,均为 v1 版本的响应: 12345678910diff-ns-service-v1diff-ns-service-v1diff-ns-service-v1diff-ns-service-v1diff-ns-service-v1diff-ns-service-v1diff-ns-service-v1diff-ns-service-v1diff-ns-service-v1diff-ns-service-v1 我们通过 kubectl 方式修改 Service 的 label: 1kubectl patch service diff-ns-service-service -p '{\"spec\":{\"selector\":{\"version\":\"v2\"}}}' 再次,执行以下命令,测试访问。 1for i in {1..10}; do curl http://diff-ns-service-service/getservicedetail?servicename=aaa; done; 返回结果如下: 12345678910diff-ns-service-v2diff-ns-service-v2diff-ns-service-v2diff-ns-service-v2diff-ns-service-v2diff-ns-service-v2diff-ns-service-v2diff-ns-service-v2diff-ns-service-v2diff-ns-service-v2 结果均为 v2 版本的响应,成功实现了蓝绿发布。 结束语云原生技术与微服务架构的天衣无缝云原生的微服务架构是云原生技术和微服务架构的完美结合。微服务作为一种架构风格,所解决的问题是交纵复杂的软件系统的架构与设计;云原生技术乃一种实现方式,所解决的问题是软件系统的运行、维护和治理。微服务架构可以选择不同的实现方式,如 Java 中的 Dubbo、Spring Cloud、Spring Cloud Alibaba,Golang 中的 Beego,Python 中的 Flask 等。但这些不同语言的服务之间的访问与运行可能存在一定得困难性与复杂性。但,云原生和微服务架构的结合,使得它们相得益彰。这其中的原因在于:云原生技术可以有效地弥补微服务架构所带来的实现上的复杂度;微服务架构难以落地的一个重要原因是它过于复杂,对开发团队的组织管理、技术水平和运维能力都提出了极高的要求。因此,一直以来只有少数技术实力雄厚的大企业会采用微服务架构。随着云原生技术的流行,在弥补了微服务架构的这一个短板之后,极大地降低了微服务架构实现的复杂度,使得广大的中小企业有能力在实践中应用微服务架构。云原生技术促进了微服务架构的推广,也是微服务架构落地的最佳搭配。 云原生时代的微服务的未来云原生的第一个发展趋势:标准化和规范化,该技术的基础是容器化和容器编排技术,最经常会用到的技术是 Kubernetes 和 Docker 等。随着云原生技术的发展,云原生技术的标准化和规范化工作正在不断推进,其目的是促进技术的发展和避免供应商锁定的问题,这对于整个云原生技术的生态系统是至关重要的。 云原生的第二个发展趋势:平台化,以服务网格技术为代表,这一趋势的出发点是增强云平台的能力,从而降低运维的复杂度。流量控制、身份认证和访问控制、性能指标数据收集、分布式服务追踪和集中式日志管理等功能,都可以由底层平台来提供,这就极大地降低了中小企业在运行和维护云原生应用时的复杂度,服务网格以 Istio 和 Linkerd 为开源代表。 云原生的第三个发展趋势:应用管理技术的进步,如在 Kubernetes 平台上部署和更新应用一直以来都比较复杂,传统的基于资源声明 YAML 文件的做法,已经逐步被 Helm 所替代。操作员模式在 Helm 的基础上更进一步,以更高效、自动化和可扩展的方式对应用部署进行管理。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: https://gitee.com/damon_one/spring-cloud-k8s 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"云原生","slug":"云原生","permalink":"http://damon008.github.io/tags/%E4%BA%91%E5%8E%9F%E7%94%9F/"},{"name":"微服务架构","slug":"微服务架构","permalink":"http://damon008.github.io/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%9E%B6%E6%9E%84/"}]},{"title":"Springboot 升级到 2.6.1 的坑","date":"2021-12-14T07:46:48.000Z","path":"2021/12/14/springboot-2-6-x/","text":"SpringBoot 2.6.0 重磅发布重要特性1. Servlet 应用支持在 Cookie 中配置 SameSite 属性该属性可通过 server.session.cookie.same-site 属性来配置,共有三个可选值: Strict 严格模式,必须同站请求才能发送 cookie Lax 宽松模式,安全的跨站请求可以发送 cookie None 禁止 SameSite 限制,必须配合 Secure 一起使用 2. 支持为主应用端口和管理端口配置健康组这在 Kubernetes 等云服务环境中很有用。在这种环境下,出于安全目的,为执行器端点使用单独的管理端口是很常见的。拥有单独的端口可能会导致不可靠的健康检查,因为即使健康检查成功,主应用程序也可能无法正常工作。 以往传统的配置会将所有 Actuator 端点都放在一个单独的端口上,并将用于检测应用状态的健康组放在主端口的附加路径下。 3. 增强/info 端点,加入 Java Runtime 信息增强后的例子: 123456789101112131415{ \"java\": { \"vendor\": \"BellSoft\", \"version\": \"17\", \"runtime\": { \"name\": \"OpenJDK Runtime Environment\", \"version\": \"17+35-LTS\" }, \"jvm\": { \"name\": \"OpenJDK 64-Bit Server VM\", \"vendor\": \"BellSoft\", \"version\": \"17+35-LTS\" } }} 该信息可以通过这个属性开启或关闭: 1management.info.java.enabled=true 4. 支持使用 WebTestClient 来测试 Spring MVC开发人员可以使用 WebTestClient 在模拟环境中测试 WebFlux 应用程序,或针对实时服务器测试任何 Spring Web 应用程序。 这次增强后,开发者可以在 Mock 环境中使用 @AutoConfigureMockMvc 注释的类,就可以轻松注入 WebTestClient。 这样编写测试就比之前容易多了。 5. 增加 spring-rabbit-stream 的自动化配置这次更新添加了 Spring AMQP 的新 spring-rabbit-stream 模块的自动配置。 当 spring.rabbitmq.listener.type 属性设置为 stream 时, StreamListenerContainer 是自动配置的。 1spring.rabbitmq.stream.*属性可用于配置对broker的访问,spring.rabbitmq.listener.stream.native-listener 可用于启用native listener 6. 支持/env 端点和 configprops 配置属性的自定义脱敏虽然 Spring Boot 之前已经可以处理 /env 和 /configprops 端点中存在的敏感值,只需要可以通过配置属性来控制即可。但还有一种情况,用户可能希望根据属性源自哪个 PropertySource 来应用清理。 例如,Spring Cloud Vault 使用 Vault 来存储加密值并将它们加载到 Spring 环境中。由于所有值都是加密的,因此将整个属性源中的每个键的值脱敏是有意义的。可以通过添加类型为 SanitizingFunction 的 @Bean 来配置此类自定义脱敏规则。 顺手推荐一下我一直在连载的免费教程:Spring Boot 教程可以点击直达!。 跟很多其他教程不同。这个教程不光兼顾了 1.x 和 2.x 版本。同时,对于每次的更新,都会选择一些相关内容修补 Tips,所以对各种不同阶段的读者长期都会有一些收获。如果你觉得不错,记得转发支持一下! 同时,如果你对哪方面还有疑惑,或希望 DD 多做些什么例子到教程里,也欢迎留言催更,或点赞支持别人的催更内容! 其他变更1. Reactive Session 个性化当前版本可以动态配置 reactive session 的有效期 1server.reactive.session.timeout=30 2. Redis 链接自动配置链接池当应用依赖中包含 commons-pool2.jar 会自动配置 redis 链接池 (Jedis Lettuce 都支持)。如果你想关闭则通过如下属性: 123spring.redis.jedis.pool.enabled=falsespring.redis.lettuce.pool.enabled=false 3. 构建信息个性化通过 spring-boot-maven-plugin 支持自动生成此次构建信息的 build-info.properties 123456789<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludeInfoProperties> <excludeInfoProperty>version</excludeInfoProperty> </excludeInfoProperties> </configuration></plugin> 4. Metrics 新增指标应用启动的两个新指标: 123application.started.time: 启动应用程序所需的时间application.ready.time: 启动应用到对外提供服务所需时间 磁盘空间的两个指标: 123disk.free: 磁盘空闲空间disk.total: 磁盘总空间 5. Docker 镜像的构建增强 docker-maven-plugin 插件的功能: 为自定义镜像设置 tags 标签 网络配置参数,可用于 Cloud Native Buildpacks 的构建过程 支持使用 buildCache 和 launchCache 配置参数自定义用于缓存层的名称,这些层由构建包提供给构建的镜像 6. 移除 2.4 版本中的过期属性由于 2.4 版本完成历史使命,因此有大量过期属性被移除,最近要升级的小伙伴一定要关注一下这部分内容,因为你原来的配置会失效! 因为内容较多,这里就不完全贴出来了,有兴趣的可以看看文末参考资料中的官方信息。 7. 默认情况完全禁止 Bean 的循环引用在 2.6.0 之后,默认禁止了循环引用,如果你的项目出现循环引用,会进行报错: 123456789101112131415The dependencies of some of the beans in the application context form a cycle: authorizationServerConfig (field private org.springframework.security.crypto.password.PasswordEncoder com.leinao.config.AuthorizationServerConfig.passwordEncoder) ↓ securityConfig ↓ org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration┌─────┐| com.alibaba.cloud.sentinel.SentinelWebAutoConfiguration (field private java.util.Optional com.alibaba.cloud.sentinel.SentinelWebAutoConfiguration.sentinelWebInterceptorOptional)└─────┘Action:Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true. 这里对阿里巴巴的 Sentinel 进行了循环引用,导致出现错误。 当然,可能这样的要求对于开发者会很痛苦。所以,你也可以通过下面的配置,放开不允许循环依赖的要求: 1spring.main.allow-circular-references=true 8. SpringMVC 默认路径匹配策略Spring MVC 处理程序映射匹配请求路径的默认策略已从 AntPathMatcher 更改为 PathPatternParser。 Actuator 端点现在也使用基于 PathPattern 的 URL 匹配。需要注意的是,Actuator 端点的路径匹配策略无法通过配置属性进行配置。 如果需要将默认切换回 AntPathMatcher,可以将 spring.mvc.pathmatch.matching-strategy 设置为 ant-path-matcher,比如下面这样: 1spring.mvc.pathmatch.matching-strategy=ant-path-matcher springboot 2.6.111 月 29 日 Spring Boot 2.6.1 正式发布,主要是为了支持本周即将发布的 Spring Cloud 2021,此版本包括 11 个错误修复和文档改进。 Bug 修复 模式分析 PatternParseException 的操作消息中的 matching-strategy 属性的名称不正确#28839 修复 ErrorPageSecurityFilter 部署到 Servlet 3.1 的兼容问题#28790 QuartzDataSourceScriptDatabaseInitiializer 不提供 MariaDB #28779 的映射 “test” 和 “Inlined Test Properties” 属性源顺序不正确#28776 在没有 spring-security-web 的 Servlet 应用程序中使用 Spring Security 时出现 ArrayStoreException #28774 DefaultClientResources 在将 Lettuce 与 Actuator 一起使用时未正确关闭是发出警告 #28767 具有 permitAll 的页面无法再通过自动配置的 MockMvc #28759 依赖管理 org.elasticsearch.distribution.integ-test-zip:elasticsearch 应将其类型声明为 zip #28746 修复文档 修复文档 “External Application Properties” 部分中的拼写错误 #28834 修复参考文档 #28833 中 “spring –version” 的输出。 org.springframework.boot.actuate.metrics.data 包添加描述 #28761。 升级 spring-cloud-k8s服务提供者(SSO)首先引入高版本: 123456<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.1</version> <relativePath/> </parent> 接下来,我们看看 cloud 如何接入 k8s,这里已经发生重大改变: 1234567891011<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>2021.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> cloud 我们引入最新版本 2021.0.0,接下来,我们看看 k8s 部分依赖: 12345678910111213141516171819202122232425 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-kubernetes-fabric8-config</artifactId> </dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-commons</artifactId> </dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-kubernetes-fabric8-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-kubernetes-fabric8-loadbalancer</artifactId></dependency><dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>${okhttp.version}</version></dependency> 这里发生改变的是,之前引入的是直接对 k8s 的,这里是关于 fabric8 第三方的注入。 引入完这些后,我们接下来看关于 Oauth2 的使用,之前引入的是: 1234<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> 这种通过注解方式来处理,但在 spring cloud2021 版本中,已经摒弃了这种方式,统一归纳到 spring-cloud-common,这里为了解决这种问题使用以下依赖: 1234<dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId></dependency> 服务提供者目前未出现任何严重问题,我们打包部署下: 看到上面的日志,表明已经部署成功。 服务消费者同样引入你那些版本依赖,如上面的服务提供者一样,同时,也需要引入: 1234<dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId></dependency> 以实现 SSO,目前看到未出现任何问题,接下来,我们需要去部署: 部署完后,发现报以上的错,经过各种尝试发现是与 swagger 的兼容,这里直接尝试:修复方法:注解 EnableSwagger2 改为 EnableWebMvc: 同时,我们发现下面的红色部分会出现问题,因为是改变了函数参数,修改为红色部分即可。 最后,我们再次启动,发现完美开启,升级成功。 PS:springboot高版本结合alibaba,目前发现一些问题,暂时不提供升级。待后续。。。","tags":[{"name":"后端","slug":"后端","permalink":"http://damon008.github.io/tags/%E5%90%8E%E7%AB%AF/"}]},{"title":"Log4j 史诗级漏洞来袭,已引起大规模入侵","date":"2021-12-10T05:58:04.000Z","path":"2021/12/10/log4j2/","text":"漏洞描述Apache Log4j2 是一个基于 Java 的日志记录工具。该工具重写了 Log4j 框架,并且引入了大量丰富的特性。该日志框架被大量用于业务系统开发,用来记录日志信息。 在大多数情况下,开发者可能会将用户输入导致的错误信息写入日志中。攻击者利用此特性可通过该漏洞构造特殊的数据请求包,最终触发远程代码执行。由于该漏洞影响范围极广,建议广大用户及时排查相关漏洞,经过白帽汇安全研究院分析确认,目前市面有多款流行的系统都受影响。 该漏洞危害等级:严重1、漏洞简介Apache Log4j 2是一款优秀的Java日志框架。该工具重写了Log4j框架,并且引入了大量丰富的特性。该日志框架被大量用于业务系统开发,用来记录日志信息。由于Apache Log4j 2某些功能存在递归解析功能,攻击者可直接构造恶意请求,触发远程代码执行漏洞。 2、漏洞危害漏洞利用无需特殊配置,攻击者可直接构造恶意请求,触发远程代码执行漏洞。 3、漏洞编号暂无 4、影响范围Apache Log4j 2.x <= 2.14.1 5、修复措施建议排查Java应用是否引入log4j-api , log4j-core 两个jar,若存在使用,极大可能会受到影响,强烈建议受影响用户尽快进行防护 。升级Apache Log4j 2所有相关应用到最新的 log4j-2.15.0-rc1 版本,地址:https://github.com/apache/logging-log4j2/releases/tag/log4j-2.15.0-rc2 升级已知受影响的应用及组件,如: spring-boot-strater-log4j2 Apache Solr Apache Flink Apache Druid 6、紧急缓解措施:如果还来不及更新版本修复,可通过下面的方法紧急缓解问题 修改jvm参数 -Dlog4j2.formatMsgNoLookups=true 修改配置:log4j2.formatMsgNoLookups=True 将系统环境变量 FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS 设置为 true","tags":[{"name":"Java","slug":"Java","permalink":"http://damon008.github.io/tags/Java/"}]},{"title":"云原生基础架构K8s实践Chat(填坑、实战版)","date":"2021-12-06T07:58:32.000Z","path":"2021/12/06/cloud-native-k8s/","text":"旧时代的微服务分布式多模块组件化太多微服务太杂难以部署,部署困难手动运维维护复杂、烧脑新时代的基础架构基础架构新模式K8s实现分布式配置化实现分布式微服务部署实现实时监控实现分布式、高可用、自启动实现实时动态更新配置实现千万服务统一管理实战云原生微服务基础架构本篇《云原生基础架构实践》Chat 集成云原生基础架构(K8s)特点实现以上功能、特性,加上基础架构的鉴权、授权、分布式互调等多维度进行实战讲解,解决微服务中多重组件复杂化形式,让微服务们飞一会儿。","tags":[{"name":"Chat","slug":"Chat","permalink":"http://damon008.github.io/tags/Chat/"},{"name":"K8s","slug":"K8s","permalink":"http://damon008.github.io/tags/K8s/"},{"name":"微服务","slug":"微服务","permalink":"http://damon008.github.io/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"}]},{"title":"WebClient 增删改查","date":"2021-11-16T07:27:25.000Z","path":"2021/11/16/webclient-restful-api/","text":"Webclient 使用场景前面介绍了 什么是阻塞、非阻塞,以及对应的客户端库,非阻塞在高并发、内存不足的情况下,还是一个不错的选择,当被访问者的服务响应很慢、或者自己在请求对方时,并不是很想知道对方返回的结果,都可以使用 Webclient 来进行非阻塞式请求。下面紧接着讲非阻塞客户端库 Webclient如何实现增删改查。 Webclient 的RestFul 请求一、RESTful风格与HTTP method熟悉RESTful风格的朋友,应该了解RESTful风格API使用HTTP method表达对资源的操作。 常用HTTP方法 RESTful风格语义(操作) POST 新增、提交数据 DELETE 删除数据 PUT 更新、修改数据 GET 查询、获取数据 下面我们就来讲下这些资源场景的使用方式。 POSTPOST等常见使用如下方法: block()阻塞获取响应结果的方法 subscribe()非阻塞异步结果订阅方法 retrieve()获取HTTP响应体,exchange()除了获取HTTP响应体,还可以获取HTTP 状态码、headers、cookies等HTTP报文信息。 使用Mono接收单个对象的响应结果,使用Flux接收集合类对象的响应结果。 占位符语法传参方式 模拟表单提交数据123456789101112131415public void testFormSubmit() { MultiValueMap<String, String> map = new LinkedMultiValueMap<>(); map.add(\"username\", \"damoin\"); map.add(\"UID\", \"11024319902323\"); Mono<String> mono = webClientBuilder.build().post() .uri(\"http://rest-service-service/add\") .contentType(MediaType.APPLICATION_FORM_URLENCODED) .body(BodyInserters.fromFormData(map)) .retrieve() .bodyToMono(String.class); System.out.println(mono.block());} 如上所示,在提交表单的时候,需要说明表单数据类型,以及表单的具体数据,我们知道:常见的表单数据都是以map形式存在,在请求后要想获取响应返回,可以使用retrieve函数,同时可以借助Mono来对返回结果进行类型转换,如果是单个对象使用Mono,如果是集合流,可以使用Flux。同时,如果想要阻塞拿到返回结果的信息,可以通过block函数来处理。 传输对象以JSON数据形式发送1234567891011121314public void testPostJson() { SysUser user = new SysUser(); user.setRealName(\"dwdwdww\"); user.setPhone(\"32323232\"); Mono<String> mono = webClientBuilder.build() .post() .uri(\"http://rest-service-service/add\") .contentType(MediaType.APPLICATION_JSON) .bodyValue(user) .retrieve() .bodyToMono(String.class); System.out.println(mono.block()); } 这里将传输的数据以Json格式来进行发送给对方,同样需要注明数据类型MediaType.APPLICATION_JSON,其它的函数都是跟上面一样。 模拟向服务端发送JSON字符串数据如果有时候对方需要的不是一个JSON对象,可能是需要一个JSON字符串,那怎么办呢? 123456789101112public void testPostJsonStr() { String jsonStr = \"{\\\"realName\\\": \\\"damon\\\",\\\"phone\\\": \\\"32323232\\\"}\"; Mono<String> mono = webClientBuilder.build().post() .uri(\"http://rest-service-service/add\") .contentType(MediaType.APPLICATION_JSON) .body(BodyInserters.fromValue(jsonStr)) .retrieve() .bodyToMono(String.class); // 输出结果 System.out.println(mono.block());} 此时,数据类型同样还是 MediaType.APPLICATION_JSON,但传输的是JSON串。 DELETE使用 DELETE方法去删除资源,删除一个已经存在的资源,使用webClient的delete()方法。该方法会向URL代表的资源发送一个HTTP DELETE方法请求: 12345public void testDelete() { webClientBuilder.build() .delete() .uri(\"http://rest-service-service/1\");} PUT修改一个已经存在的资源,使用webClient的put()方法。该方法会向URL代表的资源发送一个HTTP PUT方法请求: 12345678910111213public void testPut() { SysUser user = new SysUser(); user.setRealName(\"dwdwdww\"); user.setPhone(\"32323232\"); Mono<String> mono = webClientBuilder.build() .put() .uri(\"http://rest-service-service/1\") .contentType(MediaType.APPLICATION_JSON) .bodyValue(user).retrieve().bodyToMono(String.class); System.out.println(mono.block());} 这里以传json数据的格式来进行发送修改,修改完成后返回修改结果信息。 GET新增完数据后,我们来查看数据对象,如果是一个对象数据的话,可以使用 Mono: 1234567891011@GetMapping(value = \"/getClientResByWebClient2\", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Mono<String> getClientResByWebClient2() throws Exception { Mono<String> resp = webClientBuilder.build() .get() .uri(\"http://diff-ns-service-service/all/getService\") .retrieve().bodyToMono(String.class); //.exchange().flatMap(clientResp -> clientResp.bodyToMono(String.class)); resp.subscribe(body -> System.out.println(body)); return resp; } 如果是多个对象,那就是集合集,此时需要用Flux来获取: 12345678910public void testFlux() { Flux<SysUser> flux = webClientBuilder.build() .get() .uri(\"http://diff-ns-service-service/all\") .retrieve() .bodyToFlux(SysUser.class); List<SysUser> li = flux.collectList().block(); assert li != null; System.out.println(\"li集合元素数量:\" + li.size());} 结束福利 开源实战利用 k8s 作微服务的架构设计代码: https://gitee.com/damon_one/spring-cloud-k8s 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"Java","slug":"Java","permalink":"http://damon008.github.io/tags/Java/"},{"name":"响应式编程","slug":"响应式编程","permalink":"http://damon008.github.io/tags/%E5%93%8D%E5%BA%94%E5%BC%8F%E7%BC%96%E7%A8%8B/"}]},{"title":"阻塞与非阻塞式客户端","date":"2021-11-15T03:42:07.000Z","path":"2021/11/15/non-block-client/","text":"阻塞与非阻塞阻塞是指程序会一直等待该进程或线程完成当前任务期间不做其它事情。而非阻塞,是指当前线程在处理一些事情的同时,还可以处理其它的事情,并不需要等待当前事件完成才执行其它事件。 阻塞与非阻塞客户端对于请求当中,我们有需要借助一些请求封装的客户端,这里可以分为两大类:阻塞式、非阻塞式。 阻塞式客户端以常见的 RestTemplate为例,这是一种常见的客户端请求封装,要创建负载平衡RestTemplate,下面看看其Bean: 12345@LoadBalanced@Beanpublic RestTemplate restTemplate() { return new RestTemplate();} 在底层,RestTemplate 使用了基于每个请求对应一个线程模型(thread-per-request)的 Java Servlet API。在阻塞客户端中,这意味着,直到 Web 客户端收到响应之前,线程都将一直被阻塞下去。而阻塞带来的问题是:每个线程都消耗了一定的内存和 CPU 周期。 如果在并发下,等待结果的请求迟早都会堆积起来。这样,程序将创建很多线程,这些线程将耗尽线程池或占用所有可用内存。由于频繁的 CPU 线程切换,我们还会遇到性能下降的问题。 这在 Spring5 中,提出了一种新的客户端抽象:反应式客户端 WebClient,而 WebClient 使用了 Spring Reactive Framework 所提供的异步非阻塞解决方案。所以,当 RestTemplate创建一个个新的线程时,Webclient是为其创建类似task的线程,并且在底层,Reactive 框架将对这些 task 进行排队,并且仅在适当的响应可用时再执行它们。WebClient 是 Spring WebFlux 库的一部分。所以,我们还可以使用了流畅的函数式 API 编程,并将响应类型作为声明来进行组合。如果需要使用 WebClient,同样可以创建: 12345@Bean@LoadBalancedpublic WebClient.Builder loadBalancedWebClientBuilder() { return WebClient.builder();} 案例假设这里有一个响应非常慢的服务rest-service,我们分别用阻塞式、非阻塞式客户端来测试一下。 阻塞式我们利用 RestTemplate实现阻塞式请求: 123456789101112131415161718192021222324252627282930313233@Bean@LoadBalancedpublic RestTemplate restTemplate() {return new RestTemplate();}@AutowiredRestTemplate restTemplate;@GetMapping(\"/getClientRes\")public Response<Object> getClientRes() throws Exception { System.out.println(\"block api enter\"); HttpHeaders headers = new HttpHeaders(); MediaType type = MediaType.parseMediaType(\"application/json; charset=UTF-8\"); headers.setContentType(type); headers.add(\"Accept\", MediaType.APPLICATION_JSON.toString()); HttpEntity<String> formEntity = new HttpEntity<String>(null, headers); String body = \"\"; try { ResponseEntity<String> responseEntity = restTemplate.exchange(\"http://diff-ns-service-service/getservicedetail?servicename=cas-server-service\", HttpMethod.GET, formEntity, String.class); System.out.println(JSON.toJSONString(responseEntity)); if (responseEntity.getStatusCodeValue() == 200) { System.out.println(\"block api exit\"); return Response.ok(responseEntity.getBody()); } } catch (Exception e) { System.out.println(e.getMessage()); } System.out.println(\"block api failed, exit\"); return Response.error(\"failed\");} 在启动服务请求后,发现其打印: 12345block api enter[{\"host\":\"10.244.0.55\",\"instanceId\":\"71f96128-3bb1-11ec-97e6-ac1f6ba00d36\",\"metadata\":{\"kubectl.kubernetes.io/last-applied-configuration\":\"{\\\"apiVersion\\\":\\\"v1\\\",\\\"kind\\\":\\\"Service\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"name\\\":\\\"cas-server-service\\\",\\\"namespace\\\":\\\"system-server\\\"},\\\"spec\\\":{\\\"ports\\\":[{\\\"name\\\":\\\"cas-server01\\\",\\\"port\\\":2000,\\\"targetPort\\\":\\\"cas-server01\\\"}],\\\"selector\\\":{\\\"app\\\":\\\"cas-server\\\"}}}\\n\",\"port.cas-server01\":\"2000\",\"k8s_namespace\":\"system-server\"},\"namespace\":\"system-server\",\"port\":2000,\"scheme\":\"http\",\"secure\":false,\"serviceId\":\"cas-server-service\",\"uri\":\"http://10.244.0.55:2000\"},{\"host\":\"10.244.0.56\",\"instanceId\":\"71fc1c14-3bb1-11ec-97e6-ac1f6ba00d36\",\"metadata\":{\"$ref\":\"$[0].metadata\"},\"namespace\":\"system-server\",\"port\":2000,\"scheme\":\"http\",\"secure\":false,\"serviceId\":\"cas-server-service\",\"uri\":\"http://10.244.0.56:2000\"}]block api exit 非阻塞式上面的打印符合我们的逾期,接下来我们来看看非阻塞、反应式客户端请求: 123456789101112131415161718@Bean@LoadBalancedpublic WebClient.Builder loadBalancedWebClientBuilder() { return WebClient.builder();}@GetMapping(value = \"/getClientResByWebClient\", produces = MediaType.TEXT_EVENT_STREAM_VALUE)public Mono<String> getClientResByWebClient() throws Exception { System.out.println(\"no block api enter\"); Mono<String> resp = webClientBuilder.build().get() .uri(\"http://diff-ns-service-service/getservicedetail?servicename=cas-server-service\").retrieve() .bodyToMono(String.class); resp.subscribe(body -> System.out.println(body.toString())); System.out.println(\"no block api exit\"); return resp;} 执行完代码后,看打印: 12345no block api enterno block api exit[{\"host\":\"10.244.0.55\",\"instanceId\":\"71f96128-3bb1-11ec-97e6-ac1f6ba00d36\",\"metadata\":{\"kubectl.kubernetes.io/last-applied-configuration\":\"{\\\"apiVersion\\\":\\\"v1\\\",\\\"kind\\\":\\\"Service\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"name\\\":\\\"cas-server-service\\\",\\\"namespace\\\":\\\"system-server\\\"},\\\"spec\\\":{\\\"ports\\\":[{\\\"name\\\":\\\"cas-server01\\\",\\\"port\\\":2000,\\\"targetPort\\\":\\\"cas-server01\\\"}],\\\"selector\\\":{\\\"app\\\":\\\"cas-server\\\"}}}\\n\",\"port.cas-server01\":\"2000\",\"k8s_namespace\":\"system-server\"},\"namespace\":\"system-server\",\"port\":2000,\"scheme\":\"http\",\"secure\":false,\"serviceId\":\"cas-server-service\",\"uri\":\"http://10.244.0.55:2000\"},{\"host\":\"10.244.0.56\",\"instanceId\":\"71fc1c14-3bb1-11ec-97e6-ac1f6ba00d36\",\"metadata\":{\"$ref\":\"$[0].metadata\"},\"namespace\":\"system-server\",\"port\":2000,\"scheme\":\"http\",\"secure\":false,\"serviceId\":\"cas-server-service\",\"uri\":\"http://10.244.0.56:2000\"}] 在本例中,WebClient 返回一个 Mono 生产者后完成方法的执行。如果一旦结果可用,发布者将开始向其订阅者发送数据。调用这个API的客户端(浏览器)也将订阅返回的 Mono 对象。 阻塞式转非阻塞式可以将前面的阻塞式请求,直接转为非阻塞请求,前提是你使用的是 Spring5,此时,可以直接这样来写,贴代码: 12345@GetMapping(\"/hello\")public Mono<String> hello() { return Mono.fromCallable(() -> restTemplate.getForObject(\"http://diff-ns-service-service/all/getService\", String.class)) .subscribeOn(Schedulers.elastic());} 这样后,在请求访问时,直接返回了提供者服务返回的信息体: 1{\"result\":{\"status\":200,\"code\":0,\"msg\":\"success\"},\"data\":[{\"host\":\"10.244.0.55\",\"instanceId\":\"71f96128-3bb1-11ec-97e6-ac1f6ba00d36\",\"metadata\":{\"kubectl.kubernetes.io/last-applied-configuration\":\"{\\\"apiVersion\\\":\\\"v1\\\",\\\"kind\\\":\\\"Service\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"name\\\":\\\"cas-server-service\\\",\\\"namespace\\\":\\\"system-server\\\"},\\\"spec\\\":{\\\"ports\\\":[{\\\"name\\\":\\\"cas-server01\\\",\\\"port\\\":2000,\\\"targetPort\\\":\\\"cas-server01\\\"}],\\\"selector\\\":{\\\"app\\\":\\\"cas-server\\\"}}}\\n\",\"port.cas-server01\":\"2000\",\"k8s_namespace\":\"system-server\"},\"namespace\":\"system-server\",\"port\":2000,\"scheme\":\"http\",\"secure\":false,\"serviceId\":\"cas-server-service\",\"uri\":\"http://10.244.0.55:2000\"},{\"host\":\"10.244.0.56\",\"instanceId\":\"71fc1c14-3bb1-11ec-97e6-ac1f6ba00d36\",\"metadata\":{\"$ref\":\"$[0].metadata\"},\"namespace\":\"system-server\",\"port\":2000,\"scheme\":\"http\",\"secure\":false,\"serviceId\":\"cas-server-service\",\"uri\":\"http://10.244.0.56:2000\"}]} 这里需要注意的是,请求时,需要直接返回服务提供者的标准信息体,不能再作二次封装返回,否则,只能拿到信息: 1{\"result\":{\"status\":200,\"code\":0,\"msg\":\"success\"},\"data\":{\"scanAvailable\":true}} 表示本次 callable 为true,但这不是我们需要的信息,我们还是需要其本身返回的业务数据。所以需要提供者的返回标准化,因为直接将信息返回给可接收的浏览器等前端。 结论在大部分场景下, RestTemplate 还是继续被使用的,但有些场景下,反应式非阻塞请求还是必须的,系统资源要少得多。WebClient不失为是一个更好的选择。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: https://gitee.com/damon_one/spring-cloud-k8s 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"Java","slug":"Java","permalink":"http://damon008.github.io/tags/Java/"},{"name":"响应式编程","slug":"响应式编程","permalink":"http://damon008.github.io/tags/%E5%93%8D%E5%BA%94%E5%BC%8F%E7%BC%96%E7%A8%8B/"}]},{"title":"注意了,ribbon将被替换","date":"2021-11-15T03:39:19.000Z","path":"2021/11/15/spring-cloud-loadbalancer/","text":"区别- RibbonRibbon 是由 Netflix 发布的负载均衡器,它有助于控制 HTTP 和 TCP 的客户端的行为。Ribbon 属于客户端负载均衡。大家都知道,在我们最早使用 Springcloud 微服务架构时,就是使用 Netflix 公司的荣誉出品:https://docs.spring.io/spring-cloud-netflix/docs/2.2.9.RELEASE/reference/html/。但可惜的是,Eureka 早就正式被官方废弃,不再更新了。这也许是为了更好的统一架构。 - Spring-cloud-loadbalancerSpring-cloud-loadbalancer,是官方正式推出的一款新负载均衡利器。早在 2017 年 spring 开始尝试开发spring-cloud-loadbalancer 替代 ribbon,项目托管在 spring-cloud-incubator 孵化器,而后,经过一段时间,突然把此项目标记成归档迁移到spring-cloud-commons,说明官方在做统一公共基础架构的决心在一步步前进。 早在 Spring Cloud Hoxton.M2,第一个整合spring-cloud-loadbalancer来替换老的 ribbon: 123Spring Cloud Hoxton.M2 is the first release containing both blocking and non-blocking load balancer client implementations as an alternative to Netflix Ribbon which has entered maintenance mode.To use the new `BlockingLoadBalancerClient` with a `RestTemplate` you will need to include `org.springframework.cloud:spring-cloud-loadbalancer` on your application’s classpath. The same dependency can be used in a reactive application when using `@LoadBalanced WebClient.Builder` - the only difference is that Spring Cloud will auto-configure a `ReactorLoadBalancerExchangeFilterFunction` instance. See the [documentation](https://cloud.spring.io/spring-cloud-static/spring-cloud-commons/2.2.0.M2/reference/html/#_spring_resttemplate_as_a_load_balancer_client) for additional information. The new `ReactorLoadBalancerExchangeFilterFunction` can also be autowired and passed directly to `WebClient.Builder` (see the [documentation](https://cloud.spring.io/spring-cloud-commons/reference/html/#webflux-with-reactive-loadbalancer)). For all these features, [Project Reactor](https://projectreactor.io/)-based `RoundRobinLoadBalancer` is used underneath. 从这段原文可以看到,目前只支持BlockingLoadBalancerClient,同样是基于 RestTemplate。我们知道 ribbon 也是基于RestTemplate: 123456789@LoadBalanced@Beanpublic RestTemplate restTemplate() { SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); requestFactory.setReadTimeout(env.getProperty(\"client.http.request.readTimeout\", Integer.class, 15000)); requestFactory.setConnectTimeout(env.getProperty(\"client.http.request.connectTimeout\", Integer.class, 3000)); RestTemplate rt = new RestTemplate(requestFactory); return rt;} 但对于配置,ribbon 显然还是较老练: 12345678910111213141516backend: ribbon: client: enabled: true ServerListRefreshInterval: 5000ribbon: ConnectTimeout: 3000 ReadTimeout: 1000 eager-load: enabled: true clients: cas-server,customer-server MaxAutoRetries: 2 MaxAutoRetriesNextServer: 3 OkToRetryOnAllOperations: true NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule 可以多维度配置:超时、刷新服务列表、重试机制等。 但对于spring-cloud-loadbalancer,可以没有那么好,毕竟是刚养大的崽。但 Spring Cloud Hoxton 版本中第一次引入同时支持阻塞式与非阻塞式的负载均衡器spring-cloud-loadbalancer来作为已经进入维护状态的 Netflix Ribbon。接下来,我们实战看看如何使用。 实战spring-cloud-loadbalancer在使用时,我们从原文中了解到,只需要引入org.springframework.cloud:spring-cloud-loadbalancer依赖,就可以将新的BlockingLoadBalancerClient与RestTemplate一起使用了。同时,该依赖的引入也将支持 Reactive 应用,跟其他使用一样,只需要使用@LoadBalanced来修饰WebClient.Builder即可。 我们先来引入依赖,这里用的是基于 Nacos 的服务注册与发现,我们先来注入依赖: 1234567891011121314<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <exclusions> <exclusion> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </exclusion> </exclusions></dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-loadbalancer</artifactId></dependency> 在这里,我们使用到新的负载均衡器,需要排除 ribbon 依赖,不然 loadbalancer 无效。同时,我们需要禁用 ribbon 的负载均衡能力: 12345spring: cloud: loadbalancer: ribbon: enabled: false 禁用之后,我们在结合RestTemplate使用,并使用@LoadBalanced来修饰WebClient.Builder。 123456789@LoadBalanced//就不能用ip等形式来请求其他服务@Beanpublic RestTemplate restTemplate() { SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); requestFactory.setReadTimeout(env.getProperty(\"client.http.request.readTimeout\", Integer.class, 15000)); requestFactory.setConnectTimeout(env.getProperty(\"client.http.request.connectTimeout\", Integer.class, 3000)); RestTemplate rt = new RestTemplate(requestFactory); return rt;} 这个细心的同学可以开始看到:这个和 Ribbon 的配置是一样样的。此时,我们启动服务提供者、消费者即可测试。这里就不再展示了。 总结按照官方的孵化,新的负载均衡器将会取代老的 ribbon,毕竟引入了新的功能:Reactive,加入了对其的大力支持。这在性能方面有所提升。 同时,现在spring-cloud-loadbalancer还是存在一定局限的,比如: ribbon 提供几种默认的负载均衡策略 目前spring-cloud-loadbalancer 仅支持重试操作的配置 ribbon 支持超时、懒加载处理、重试及其和 hystrix 整合高级属性等 在 Spring-cloud 体系中,大部分范围还是老实使用 Ribbon,但基于 spring-cloud-k8s,可能需要使用基于spring-cloud-starter-kubernetes-loadbalancer。因为在前面实践过,基于 Ribbon 的 LB,无法跨命名空间来实现服务间的相互访问。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: https://gitee.com/damon_one/spring-cloud-k8s 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"Java","slug":"Java","permalink":"http://damon008.github.io/tags/Java/"}]},{"title":"k8s 集群从0到1","date":"2021-11-12T02:08:01.000Z","path":"2021/11/12/kubeadm-install-k8s/","text":"前言&emsp;&emsp; 本文只讲 docker + kubernetes 集群的 kubeadm 部署方式,不讲 k8s 各组件工作原理。个人认为在学习 k8s 的过程中,先动手搭建起来一个测试集群,比一味的看书学原理要好得多,一边操作一边学习是比较有效率的。 &emsp;&emsp;使用 yum 安装 docker、 kubelet、 kubeadm 包的方式,会自动部署 service 文件及环境,相比二进制安装更简单一些。二进制安装更复杂但是也更灵活一些,学习二进制安装有助于用户学习各部件工作原理,以及排错能力。下面先介绍比较简单的 yum 安装的方式。 Yum 安装包部署k8s 集群流程一、准备工作 (所有节点) 配置所有节点互通 ssh 免密(ssh-copy-id) 配置所有节点 /etc/hosts 文件统一记录所有机器 hostname 和ip。hosts 文件需要在新增节点时更新,在大型集群中最好通过 git 来同步所有机器。 1234567[root@k8s-master-1 data]# cat /etc/hosts127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4::1 localhost localhost.localdomain localhost6 localhost6.localdomain6192.168.63.131 k8s-master-1192.168.63.141 k8s-node-1192.168.63.142 k8s-node-2192.168.63.143 k8s-node-3 ntp 时间同步,k8s 很多组件工作需要保证所有节点 时间一致。 1234567# yum install -y ntpdate# 使用公网 ntp# ntpdate us.pool.ntp.org时间不对的话确认系统时区# ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 关闭 防火墙,selinux ,swap 12345678910111213141516测试环境# systemctl stop firewalld# systemctl disable firewalld正式环境(不允许关闭防火墙,保证默认规则为 ACCEPT )# iptables -L | grep ChainChain INPUT (policy ACCEPT)Chain FORWARD (policy ACCEPT)Chain OUTPUT (policy ACCEPT) # setenforce 0# vim /etc/selinux/configSELINUX=disabled# swapoff -a## 编辑/etc/fstab,注释掉包含swap的那一行即可,重启后可永久关闭 修改必要的内核参数 123456789二层的网桥在转发包时也会被iptables的FORWARD规则所过滤linux主机有多个网卡时一个网卡收到的信息是否能够传递给其他的网卡# modprobe br_netfilter# vim /etc/sysctl.d/k8s.confnet.bridge.bridge-nf-call-ip6tables = 1net.bridge.bridge-nf-call-iptables = 1net.ipv4.ip_forward = 1# sysctl -p /etc/sysctl.d/k8s.conf 配置 yum 源装包。 使用 docker-ce 官方 yum 源安装 docker-ce 软件包 123456789101112131415161718192021222324# 第一种:官方提供的脚本# curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun# 第二种:手动添加源安装$ sudo yum remove docker \\ docker-client \\ docker-client-latest \\ docker-common \\ docker-latest \\ docker-latest-logrotate \\ docker-logrotate \\ docker-engine$ sudo yum install -y yum-utils \\ device-mapper-persistent-data \\ lvm2$ sudo yum-config-manager \\ --add-repo \\ http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo$ sudo yum install docker-ce docker-ce-cli containerd.io指定版本$ sudo yum install docker-ce-18.09.1 docker-ce-cli-18.09.1 containerd.io 使用 阿里云的 yum 源安装 kubelet kubeadm kubectl 软件包 1234567891011[root@k8s-master-1 data]# cat /etc/yum.repos.d/kubernetes.repo[kubernetes]name=Kubernetesbaseurl=http://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64enabled=1gpgcheck=0repo_gpgcheck=0gpgkey=http://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg http://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg[root@k8s-master-1 data]# yum install -y kubelet-1.15.6 kubeadm-1.15.6 kubectl-1.15.6 开启docker 服务 12# systemctl start docker ; systemctl enable docker# docker run hello-world 测试docker 是否可用 开启kubelet 服务 12# systemctl enable kubelet ; systemctl start kubelet# 发现kubelet 启动报错,这是正常的,因为缺少证书,kubeadm 部署后,即可正常启动。 确保 kubelet 和 docker 的 cgroup drive 一致,使用 systemd 作为initd 的发行版本,推荐使用systemd ,cgroup 管理更加稳定 123456789101112# docker info | grep -i cgroup# cat /etc/systemd/system/kubelet.service.d/10-kubeadm.conf## 修改 docker# cat /etc/docker/daemon.json{ \"exec-opts\":[\"native.cgroupdriver=systemd\"]}## 修改 kubelet# sed -i \"s/cgroup-driver=systemd/cgroup-driver=cgroupfs/g\" /etc/systemd/system/kubelet.service.d/10-kubeadm.conf# systemctl daemon-reload 提前准备镜像,kubeadm 工具部署集群时需要用到一些镜像,默认去墙外的网站拉取,如果你的机器不能访问墙外的世界,那就需要提前准备,并且把准备的镜像打上默认需要的版本的 tag, 学习的路上总是有一些障碍的,还好阿里的源为我们准备了这些镜像 查看kubeadm 需要的所有镜像 12345678[root@k8s-master-1 data]# kubeadm config images list --kubernetes-version=v1.16.2k8s.gcr.io/kube-apiserver:v1.16.2k8s.gcr.io/kube-controller-manager:v1.16.2k8s.gcr.io/kube-scheduler:v1.16.2k8s.gcr.io/kube-proxy:v1.16.2k8s.gcr.io/pause:3.1k8s.gcr.io/etcd:3.3.15-0k8s.gcr.io/coredns:1.6.2 上面 k8s.gcr.io 的地址在墙外,我们换成阿里云地址就可以拉下来。registry.cn-hangzhou.aliyuncs.com/google_containers/kube-xxx:v1.16.2 123456kubeadm config images list |sed -e 's/^/docker pull /g' -e 's#k8s.gcr.io#docker.io/registry.cn-hangzhou.aliyuncs.com/google_containers#g' |sh -xdocker images |grep registry.cn-hangzhou.aliyuncs.com/google_containers |awk '{print \"docker tag \",$1\":\"$2,$1\":\"$2}' |sed -e 's#docker.io/mirrorgooglecontainers#k8s.gcr.io#2' |sh -xdocker images |grep registry.cn-hangzhou.aliyuncs.com/google_containers |awk '{print \"docker rmi \", $1\":\"$2}' |sh -xdocker pull coredns/coredns:1.2.2docker tag coredns/coredns:1.2.2 k8s.gcr.io/coredns:1.2.2docker rmi coredns/coredns:1.2.2 如果我们的 k8s 版本在 1.13 以上 可以直接用 –image-repository 修改 kubeadm 拉取镜像的地址。 kubeadm init –image-repository registry.aliyuncs.com/google_containers 二、部署 master 节点(master节点) 使用 kubeadm 初始化 k8s 集群 master 节点,可以看到 kubeadm 都做了那些工作。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970[root@k8s-master-1 ~]# kubeadm init --kubernetes-version=v1.16.2 --pod-network-cidr=10.244.0.0/16 --service-cidr=10.96.0.0/12;[init] Using Kubernetes version: v1.16.2[preflight] Running pre-flight checks [WARNING SystemVerification]: this Docker version is not on the list of validated versions: 19.03.4. Latest validated version: 18.09 [WARNING Hostname]: hostname \"k8s-master-1\" could not be reached [WARNING Hostname]: hostname \"k8s-master-1\": lookup k8s-master-1 on 192.168.63.2:53: server misbehaving [WARNING Service-Kubelet]: kubelet service is not enabled, please run 'systemctl enable kubelet.service'[preflight] Pulling images required for setting up a Kubernetes cluster[preflight] This might take a minute or two, depending on the speed of your internet connection[preflight] You can also perform this action in beforehand using 'kubeadm config images pull'[kubelet-start] Writing kubelet environment file with flags to file \"/var/lib/kubelet/kubeadm-flags.env\"[kubelet-start] Writing kubelet configuration to file \"/var/lib/kubelet/config.yaml\"[kubelet-start] Activating the kubelet service[certs] Using certificateDir folder \"/etc/kubernetes/pki\"[certs] Generating \"ca\" certificate and key[certs] Generating \"apiserver\" certificate and key[certs] apiserver serving cert is signed for DNS names [k8s-master-1 kubernetes kubernetes.default kubernetes.default.svc kubernetes.default.svc.cluster.local] and IPs [10.96.0.1 192.168.63.131][certs] Generating \"apiserver-kubelet-client\" certificate and key[certs] Generating \"front-proxy-ca\" certificate and key[certs] Generating \"front-proxy-client\" certificate and key[certs] Generating \"etcd/ca\" certificate and key[certs] Generating \"etcd/server\" certificate and key[certs] etcd/server serving cert is signed for DNS names [k8s-master-1 localhost] and IPs [192.168.63.131 127.0.0.1 ::1][certs] Generating \"etcd/peer\" certificate and key[certs] etcd/peer serving cert is signed for DNS names [k8s-master-1 localhost] and IPs [192.168.63.131 127.0.0.1 ::1][certs] Generating \"etcd/healthcheck-client\" certificate and key[certs] Generating \"apiserver-etcd-client\" certificate and key[certs] Generating \"sa\" key and public key[kubeconfig] Using kubeconfig folder \"/etc/kubernetes\"[kubeconfig] Writing \"admin.conf\" kubeconfig file[kubeconfig] Writing \"kubelet.conf\" kubeconfig file[kubeconfig] Writing \"controller-manager.conf\" kubeconfig file[kubeconfig] Writing \"scheduler.conf\" kubeconfig file[control-plane] Using manifest folder \"/etc/kubernetes/manifests\"[control-plane] Creating static Pod manifest for \"kube-apiserver\"[control-plane] Creating static Pod manifest for \"kube-controller-manager\"[control-plane] Creating static Pod manifest for \"kube-scheduler\"[etcd] Creating static Pod manifest for local etcd in \"/etc/kubernetes/manifests\"[wait-control-plane] Waiting for the kubelet to boot up the control plane as static Pods from directory \"/etc/kubernetes/manifests\". This can take up to 4m0s[apiclient] All control plane components are healthy after 22.511803 seconds[upload-config] Storing the configuration used in ConfigMap \"kubeadm-config\" in the \"kube-system\" Namespace[kubelet] Creating a ConfigMap \"kubelet-config-1.16\" in namespace kube-system with the configuration for the kubelets in the cluster[upload-certs] Skipping phase. Please see --upload-certs[mark-control-plane] Marking the node k8s-master-1 as control-plane by adding the label \"node-role.kubernetes.io/master=''\"[mark-control-plane] Marking the node k8s-master-1 as control-plane by adding the taints [node-role.kubernetes.io/master:NoSchedule][bootstrap-token] Using token: tacwl8.r9rybowvi486j94t[bootstrap-token] Configuring bootstrap tokens, cluster-info ConfigMap, RBAC Roles[bootstrap-token] configured RBAC rules to allow Node Bootstrap tokens to post CSRs in order for nodes to get long term certificate credentials[bootstrap-token] configured RBAC rules to allow the csrapprover controller automatically approve CSRs from a Node Bootstrap Token[bootstrap-token] configured RBAC rules to allow certificate rotation for all node client certificates in the cluster[bootstrap-token] Creating the \"cluster-info\" ConfigMap in the \"kube-public\" namespace[addons] Applied essential addon: CoreDNS[addons] Applied essential addon: kube-proxyYour Kubernetes control-plane has initialized successfully!To start using your cluster, you need to run the following as a regular user: mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/configYou should now deploy a pod network to the cluster.Run \"kubectl apply -f [podnetwork].yaml\" with one of the options listed at: https://kubernetes.io/docs/concepts/cluster-administration/addons/Then you can join any number of worker nodes by running the following on each as root:kubeadm join 192.168.63.131:6443 --token tacwl8.r9rybowvi486j94t \\ --discovery-token-ca-cert-hash sha256:83d9e155464739407fe5a782b41eac35c42fcba2a1e8c252066cd7e00eaf21ff 初始化后根据 提示 完成 config 文件的配置,并且记住 kubeadm join 命令,以便于后续添加node 节点。 123# mkdir -p $HOME/.kube# sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config# sudo chown $(id -u):$(id -g) $HOME/.kube/config 部署网络插件,使用 flannel 或者 calico 需要自己选择。calico 需要配置rbac ,新版本已经集成到 calico.yaml文件中。 12345678# kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml或# kubectl apply -f https://docs.projectcalico.org/v3.9/manifests/calico-etcd.yaml如果没有关闭防火墙需要为calico 打开端口# firewall-cmd --permanent --add-port=5543/tcp --zone=public# firewall-cmd --permanent --add-port=179/tcp --zone=public# firewall-cmd --reload 部署 dashboard web ui 管理界面。 事先准备镜像 123# docker pull registry.cn-hangzhou.aliyuncs.com/rsqlh/kubernetes-dashboard:v1.10.1# docker tag registry.cn-hangzhou.aliyuncs.com/rsqlh/kubernetes-dashboard:v1.10.1 k8s.gcr.io/kubernetes-dashboard-amd64:v1.10.1# docker rmi registry.cn-hangzhou.aliyuncs.com/rsqlh/kubernetes-dashboard:v1.10.1 应用 yaml 文件 123456789101112[root@k8s-master-1 data]# kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v1.10.1/src/deploy/recommended/kubernetes-dashboard.yamlsecret/kubernetes-dashboard-certs createdserviceaccount/kubernetes-dashboard createdrole.rbac.authorization.k8s.io/kubernetes-dashboard-minimal createdrolebinding.rbac.authorization.k8s.io/kubernetes-dashboard-minimal createddeployment.apps/kubernetes-dashboard createdservice/kubernetes-dashboard created查看pod# kubectl get pods -n kube-systemkubernetes-dashboard-57df4db6b-p9sm8 1/1 Running 0 15s 开启集群外部访问 1234567891011[root@k8s-master-1 data]# kubectl get svc -n kube-systemNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEkube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP,9153/TCP 8dkubernetes-dashboard ClusterIP 10.110.144.227 <none> 443/TCP 40s[root@k8s-master-1 data]# kubectl patch svc kubernetes-dashboard -p '{\"spec\":{\"type\":\"NodePort\"}}' -n kube-systemservice/kubernetes-dashboard patched[root@k8s-master-1 data]# kubectl get svc -n kube-systemNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEkube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP,9153/TCP 8dkubernetes-dashboard NodePort 10.110.144.227 <none> 443:30541/TCP 51s 通过 ip 和端口 访问会看到需要 token 验证或者 kubeconfig 文件验证。具体的token 获取方法需要创建 serivceaccount 然后 拿到secret 的token 即可,这里不详细阐述。 三、部署 node 节点加入集群(node节点) 使用上面记住的命令 初始化 node 节点。 12345# kubeadm join 192.168.63.131:6443 --token tacwl8.r9rybowvi486j94t --discovery-token-ca-cert-hash sha256:83d9e155464739407fe5a782b41eac35c42fcba2a1e8c252066cd7e00eaf21ff## 如果没有关闭防火墙# firewall-cmd --permanent --add-port=6443/tcp# firewall-cmd --reload 如果在node 节点需要使用kubectl 命令 ,需要为对应用户配置 config 文件。 123# mkdir -p $HOME/.kube# scp -i master:/etc/kubernetes/admin.conf $HOME/.kube/config# sudo chown $(id -u):$(id -g) $HOME/.kube/config kubectl get pod –all-namespaces 检查 pod 运行情况 参考文档https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/ https://github.com/kubernetes/dashboard 结束福利 开源实战利用 k8s 作微服务的架构设计代码: https://gitee.com/damon_one/spring-cloud-k8s 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"K8s","slug":"K8s","permalink":"http://damon008.github.io/tags/K8s/"}]},{"title":"记录那些年 Nacos 的坑","date":"2021-11-12T01:22:46.000Z","path":"2021/11/12/alibaba-nacos/","text":"Nacos 旧史12018年11月左右,Springcloud 联合创始人Spencer Gibb在Spring官网的博客页面宣布:阿里巴巴开源 Spring Cloud Alibaba,并发布了首个预览版本。随后,Spring Cloud 官方Twitter也发布了此消息。 随着这一消息发布,外面才知道 Nacos 的诞生。毕竟是大厂的 KPI 产物,我们来尝尝鲜。 一、环境准备 Spring Boot: 2.3.12 Spring Cloud: Hoxton.SR12 Spring Cloud Alibaba: 2.2.6.RELEASE Maven: 3.5.4 Java 1.8 + Oauth2 (Spring Security 5.3.9) 安装 Nacos下载 Nacos 地址:https://github.com/alibaba/nacos/releases 版本:v1.2.1 执行: 123Linux/Unix/Mac:sh startup.sh -m standaloneWindows:cmd startup.cmd -m standalone 启动完成之后,访问:http://127.0.0.1:8848/nacos/,可以进入Nacos的服务管理页面,具体如下: 在上面,我们可以看到启动的服务列表信息,同时,我们也可以去配置此服务的相关配置: 具体的配置如下: 这里,我们可以设置配置的类型,比如:yaml、properties。默认的是后者,这里我们可以设置: 123456789101112131415161718192021spring: application: name: cas-server cloud: nacos: server-addr: 127.0.0.1:8848 discovery: enabled: true namespace: a48cec97-fa0f-48e0-97c7-0aced5c7ecbe #默认public #group: mine #${nacos.runtime-env} heart-beat-interval: 10 heart-beat-timeout: 15 config: enabled: true #namespace: ${nacos.namespace} file-extension: yaml #默认properties #group: ${spring.application.name} shared-configs: - data-id: application-mysql.properties refresh: false 不同 namespace 下的服务互调在 Nacos 里,有几个概念,命令空间 namespace、分组 group 等。虽然这里的关键词跟 K8s 类似,但差别还是很大。这就是我今天想说的坑。这里我们默认配置了 properties。 在 Nacos 中,为了将不同的服务进行划分区域,这也引入了一些概念:namespace、group 我们先来设置一下 namespace,假如我们这里新建一个 namespace: 那么在注册服务时,我们把这个服务放在了这个 new-NS下,启动该服务,我们来看信息: 我们可以看到cas-server服务在 namespace 名称为 new-NS 下。 下面,我们继续加入一个新服务,把这个新服务放在默认的 namespace 下,假设利用它来调用 cas-server 服务。 接下来,我们通过命令调用服务: 1curl -i -H \"Accept: application/json\" -H \"Authorization:bearer fbbb08b5-fc9c-4bf9-a676-6a1d5d6a0dda\" -X GET http://localhost:2001/api/user/get 此时可以看到日志: 这里由于被访问的服务是需要鉴权的,所以可以看到:这里的日志是去请求统一认证鉴权中心的check_token接口。由于这两个服务都被注册到 Nacos,这里直接通过域名来请求了。 但从日志中可以看到,抛出异常:java.lang.IllegalStateException: No instances available for cas-server,这是因为无法解析到这个域名对应的 ip。但从上面,我们可以看到明明有注册信息,为啥不能访问呢? 这就是 Nacos 现在呈现的第一个坑:无法在跨namespace 下访问其他服务。如果我们把cas-server也放在默认的 namespace 下呢? 再次,我们通过命令调用服务,会发现可以正常请求了: 我们发现正常请求后,返回了信息,只不过返回的是400,这是由于我这个 token 失效了,无效的token,请重新认证访问。 同 namespace 下不同组的服务互调上面说到不同 namespace 下的服务互调的问题,接下来,我们看看同一 namespace 下不同分组的服务互调是咋样的呢? 同样的,我们假设把 cas-server 分配到一个新的 group: 1234567891011spring: application: name: cas-server cloud: nacos: server-addr: 127.0.0.1:8848 discovery: enabled: true group: mine heart-beat-interval: 10 heart-beat-timeout: 15 新服务rest-service还是放在默认分组DEFAULT_GROUP里: 接下来,我们通过命令调用服务: 1curl -i -H \"Accept: application/json\" -H \"Authorization:bearer fbbb08b5-fc9c-4bf9-a676-6a1d5d6a0dda\" -X GET http://localhost:2001/api/user/get 我们来看看日志: 发现还是跟前面说的那种情况请求后一样,仍然抛出异常:java.lang.IllegalStateException: No instances available for cas-server。 结论在 Nacos 较高版本中验证这两种情况,同样得出相同的结论:同一namespace下的不同group的服务无法相互调用,不同namespace下的同group的服务无法相互调用。 PS在这里说出 Nacos 的坑,并不是在指责 Nacos 团队哈,只是希望官方尽快出新的 feature。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: https://gitee.com/damon_one/spring-cloud-k8s 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"Java","slug":"Java","permalink":"http://damon008.github.io/tags/Java/"}]},{"title":"统一认证中心 Oauth2 高可用坑","date":"2021-11-12T00:10:32.000Z","path":"2021/11/12/oauth2-ha/","text":"前面 (统一认证中心 Oauth2 认证坑) 我们利用user-info-uri来实现消费端的认证信息以及授权获取判断,接下来我们借助 token-info-uri 来实现认证以及授权破。具体配置见: 123456789101112131415161718cas-server-url: http://cas-serversecurity: path: ignores: /,/index,/static/**,/css/**, /image/**, /favicon.ico, /js/**,/plugin/**,/avue.min.js,/img/**,/fonts/** oauth2: client: client-id: rest-service client-secret: rest-service-123 user-authorization-uri: ${cas-server-url}/oauth/authorize access-token-uri: ${cas-server-url}/oauth/token resource: loadBalanced: true id: rest-service prefer-token-info: true token-info-uri: ${cas-server-url}/oauth/check_token authorization: check-token-access: ${cas-server-url}/oauth/check_token 这里的/oauth/check_token是 Oauth2 原生自带的,这里不需要封装。接下来,我们启动服务,在拿到 token 后,通过 token 请求消费端: 123456789102021-11-03 16:40:09.057 DEBUG 24652 --- [io2-2001-exec-4] o.s.web.client.RestTemplate : HTTP POST http://cas-server/oauth/check_token2021-11-03 16:40:09.060 DEBUG 24652 --- [io2-2001-exec-4] o.s.web.client.RestTemplate : Accept=[application/json, application/*+json]2021-11-03 16:40:09.062 DEBUG 24652 --- [io2-2001-exec-4] o.s.web.client.RestTemplate : Writing [{token=[b34841b4-61fa-4dbb-9e2b-76496deb27b4]}] as \"application/x-www-form-urlencoded\"2021-11-03 16:40:11.332 ERROR 24652 --- [io2-2001-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exceptionorg.springframework.web.client.ResourceAccessException: I/O error on POST request for \"http://cas-server/oauth/check_token\": cas-server; nested exception is java.net.UnknownHostException: cas-server at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:746) at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:672) at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:581) at org.springframework.security.oauth2.provider.token.RemoteTokenServices.postForMap(RemoteTokenServices.java:149) 我们从上面的日志中,可以发现系统抛出 UnknownHostException 这种异常,无法找到cas-server,但我要说的是:我们这里用到的是Nacos注册中心来实现服务的注册与发现: 那说明注册的服务可以被发现,接下来我们看支持 LB 的几种服务消费方式: RestTemplate、WebClient、Feign。我们这里基于 Ribbon,RestTemplate,因为在Oauth2原生中,就是基于RestTemplate来调用远程服务: 1234567891011private Map<String, Object> postForMap(String path, MultiValueMap<String, String> formData, HttpHeaders headers) { if (headers.getContentType() == null) { headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); } @SuppressWarnings(\"rawtypes\") Map map = restTemplate.exchange(path, HttpMethod.POST, new HttpEntity<MultiValueMap<String, String>>(formData, headers), Map.class).getBody(); @SuppressWarnings(\"unchecked\") Map<String, Object> result = map; return result; } 大家都知道默认的原生Ribbon,是基于 RestTemplate 的负载均衡,所以这里配置如下: 123456789@LoadBalanced@Beanpublic RestTemplate restTemplate() { SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); requestFactory.setReadTimeout(env.getProperty(\"client.http.request.readTimeout\", Integer.class, 15000)); requestFactory.setConnectTimeout(env.getProperty(\"client.http.request.connectTimeout\", Integer.class, 3000)); RestTemplate rt = new RestTemplate(requestFactory); return rt;} 可以看到,在定义 RestTemplate 的时候,增加了@LoadBalanced注解,但其实在真正调用服务接口的时候,原来host部分是通过手工拼接ip和端口的,直接采用服务名的时候来写请求路径即可。在真正调用的时候,Spring Cloud会将请求拦截下来,然后通过负载均衡器选出节点,并替换服务名为具体的ip和端口,从而实现基于服务名的负载均衡调用。 接下来,我们再看看负载均衡的策略是否有问题,Ribbon默认的负载均衡策略是轮询,内置了多种负载均衡策略,内置的负载均衡的顶级接口为com.netflix.loadbalancer.IRule。具体的策略有:AvailabilityFilteringRule、RoundRobinRule、RetryRule、RandomRule、WeightedResponseTimeRule、BestAvailableRule等。这里直接使用默认的轮询: 12345678910@Beanpublic IRule ribbonRule(IClientConfig config){ //return new AvailabilityFilteringRule(); return new RoundRobinRule();//轮询 //return new RetryRule();//重试 //return new RandomRule();//这里配置策略,和配置文件对应 //return new WeightedResponseTimeRule();//这里配置策略,和配置文件对应 //return new BestAvailableRule();//选择一个最小的并发请求的server //return new MyProbabilityRandomRule();//自定义} 到这里,目前都还未发现问题,那么既然实现了基于RestTemplate的负载均衡,为什么还是报错呢? 找了半天,最后发现在Oauth2源码中,注入的是这么个玩意: 这时候才发现多么的坑,于是乎,一顿猛操作:在资源检验时调用的覆盖其注入: 12345@Autowired(required = true)private RemoteTokenServices remoteTokenServices;@AutowiredRestTemplate restTemplate; 其二,直接set RestTemplate: 1234567891011121314151617181920212223242526@Override public void configure(ResourceServerSecurityConfigurer resource) throws Exception { super.configure(resource); restTemplate.setErrorHandler(new DefaultResponseErrorHandler() { @Override // Ignore 400 public void handleError(ClientHttpResponse response) throws IOException { if (response.getRawStatusCode() != 400) { super.handleError(response); } } }); if (Objects.nonNull(remoteTokenServices)) { remoteTokenServices.setRestTemplate(restTemplate); resource.tokenServices(remoteTokenServices); } resource //.tokenStore(tokenStore) //.tokenServices(tokenServices) .authenticationEntryPoint(customAuthenticationEntryPoint) .accessDeniedHandler(customAccessDeniedHandler) //.tokenExtractor(new BearerTokenExtractor()) ; } 接下来,我们重启消费端,看看效果,根据之前请求的token,直接访问消费端接口: 123452021-11-03 16:57:50.476 INFO 81424 --- [io2-2001-exec-3] o.s.web.servlet.DispatcherServlet : Completed initialization in 12 ms2021-11-03 16:57:50.522 DEBUG 81424 --- [io2-2001-exec-3] o.s.web.client.RestTemplate : HTTP POST http://cas-server/oauth/check_token2021-11-03 16:57:50.526 DEBUG 81424 --- [io2-2001-exec-3] o.s.web.client.RestTemplate : Accept=[application/json, application/*+json]2021-11-03 16:57:50.528 DEBUG 81424 --- [io2-2001-exec-3] o.s.web.client.RestTemplate : Writing [{token=[b34841b4-61fa-4dbb-9e2b-76496deb27b4]}] as \"application/x-www-form-urlencoded\"2021-11-03 16:57:50.635 DEBUG 81424 --- [io2-2001-exec-3] o.s.web.client.RestTemplate : Response 200 OK 发现ok了,返回成功200,并且有权限访问该接口: 总结 有时候自己的代码写的已经很好了,但发现还是无法实现自己想要的:于是乎,可以大胆设想是不是官网源码出了幺蛾子,就像本文一样,如果不一步步检查,怎么也不会发现原来是源码留下如此大的坑,在前面的文章中,其实发现很多源码的不合理之处之后,都在修改,并且生成一套自己的规范返回,这样对于代码本身来说,我们会更加深刻体会、理解。Oauth2源码本身可以只是一个带头的基础功能,后面基于大项目,需要自己对于一些系统的设计进行改造,例如:高可用、高并发鉴权方案、统一认证SSO等等。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: https://gitee.com/damon_one/spring-cloud-k8s 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"Java","slug":"Java","permalink":"http://damon008.github.io/tags/Java/"}]},{"title":"统一认证中心 Oauth2 认证坑","date":"2021-11-11T07:11:21.000Z","path":"2021/11/11/oauth2/","text":"在前面文章 Springcloud Oauth2 HA篇 中,实现了基于 Oauth2 的统一认证的认证与授权。在配置中,我们可以看到: 1234567891011121314cas-server-url: http://cas-server-service #这里配置成HA地址security: oauth2: #与cas-server对应的配置 client: client-id: admin-web client-secret: admin-web-123 user-authorization-uri: ${cas-server-url}/oauth/authorize #是授权码认证方式需要的 access-token-uri: ${cas-server-url}/oauth/token #是密码模式需要用到的获取 token 的接口 resource: loadBalanced: true id: admin-web user-info-uri: ${cas-server-url}/api/user #指定user info的URI prefer-token-info: false 这里的 url 配置是基于k8s的 Service,实现负载均衡,从而实现高可用。但我们接下来分析 user-info-uri。user-info-uri 的原理是在授权服务器认证后将认证信息 Principal 通过形参绑定的方法通过URL的方式获取用户信息。当然它也有配套的 UserInfoTokenService 等。 但这个在客户端获取用户权限时候,是存在一定问题的。譬如 Web端请求消费端的某个接口: 123456789101112131415161718192021222324252627/** * 返回发现的所有服务 * @author Damon * @date 2021年11月2日 下午8:18:44 * @return * */@PreAuthorize(\"hasRole('admin')\") @GetMapping(value = \"/getService\") public String getService(){ HttpHeaders headers = new HttpHeaders(); MediaType type = MediaType.parseMediaType(\"application/json; charset=UTF-8\"); headers.setContentType(type); headers.add(\"Accept\", MediaType.APPLICATION_JSON.toString()); HttpEntity<String> formEntity = new HttpEntity<String>(null, headers); String body = \"\"; try { ResponseEntity<String> responseEntity = restTemplate.exchange(\"http://cas-server/api/v1/user\", HttpMethod.GET, formEntity, String.class); if (responseEntity.getStatusCodeValue() == 200) { return \"ok\"; } } catch (Exception e) { System.out.println(e.getMessage()); } return body; } 在这个接口中,我们通过添加@PreAuthorize(\"hasRole('admin')\")来控制权限,只要是admin的用户才能访问改接口。 我们先来请求认证中心登录接口,获取token: 在拿到token之后,我们请求这个接口,我们会发现: 说明未认证,我们再看看:发现原来当请求这个接口时,消费端后去请求认证中心的接口: 12345672021-11-03 15:59:09.385 DEBUG 127896 --- [io2-2001-exec-4] org.springframework.web.HttpLogging : HTTP GET http://cas-server/auth/user2021-11-03 15:59:09.389 DEBUG 127896 --- [io2-2001-exec-4] org.springframework.web.HttpLogging : Accept=[application/json, application/*+json]2021-11-03 15:59:09.427 DEBUG 127896 --- [io2-2001-exec-4] org.springframework.web.HttpLogging : Response 404 NOT_FOUND2021-11-03 15:59:09.446 DEBUG 127896 --- [io2-2001-exec-4] o.s.w.c.HttpMessageConverterExtractor : Reading to [org.springframework.security.oauth2.common.exceptions.OAuth2Exception]2021-11-03 15:59:09.456 WARN 127896 --- [io2-2001-exec-4] o.s.b.a.s.o.r.UserInfoTokenServices : Could not fetch user details: class org.springframework.web.client.HttpClientErrorException$NotFound, 404 : [{\"timestamp\":\"2021-11-03T07:59:09.423+00:00\",\"status\":404,\"error\":\"Not Found\",\"message\":\"\",\"path\":\"/auth/user\"}]2021-11-03 15:59:09.457 ERROR 127896 --- [io2-2001-exec-4] c.l.h.CustomAuthenticationEntryPoint : 无效的token,请重新认证访问{\"data\":\"b34841b4-61fa-4dbb-9e2b-76496deb27b4\",\"result\":{\"code\":20202,\"msg\":\"未认证\",\"status\":401}} 但认证中心给返回的404状态码,此时会走统一异常EntryPoint提示报错:无效的token,请重新认证访问。从而返回信息体:{\"data\":\"b34841b4-61fa-4dbb-9e2b-76496deb27b4\",\"result\":{\"code\":20202,\"msg\":\"未认证\",\"status\":401}}。 接下来分析:为什么认证中心会返回404呢?看认证中心日志: 1234567892021-11-03 15:59:09.407 DEBUG 54492 --- [o2-2000-exec-15] o.s.web.servlet.DispatcherServlet : GET \"/auth/user\", parameters={}2021-11-03 15:59:09.409 DEBUG 54492 --- [o2-2000-exec-15] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped to ResourceHttpRequestHandler [\"classpath:/META-INF/resources/\", \"classpath:/resources/\", \"classpath:/static/\", \"classpath:/public/\"]2021-11-03 15:59:09.413 DEBUG 54492 --- [o2-2000-exec-15] o.s.w.s.r.ResourceHttpRequestHandler : Resource not found2021-11-03 15:59:09.414 DEBUG 54492 --- [o2-2000-exec-15] o.s.web.servlet.DispatcherServlet : Completed 404 NOT_FOUND2021-11-03 15:59:09.422 DEBUG 54492 --- [o2-2000-exec-15] o.s.web.servlet.DispatcherServlet : \"ERROR\" dispatch for GET \"/error\", parameters={}2021-11-03 15:59:09.423 DEBUG 54492 --- [o2-2000-exec-15] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)2021-11-03 15:59:09.424 DEBUG 54492 --- [o2-2000-exec-15] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [application/json] and supported [application/json, application/*+json, application/json, application/*+json, application/json, application/*+json]2021-11-03 15:59:09.424 DEBUG 54492 --- [o2-2000-exec-15] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [{timestamp=Wed Nov 03 15:59:09 CST 2021, status=404, error=Not Found, message=, path=/auth/user}]2021-11-03 15:59:09.426 DEBUG 54492 --- [o2-2000-exec-15] o.s.web.servlet.DispatcherServlet : Exiting from \"ERROR\" dispatch, status 404 发现原来Oauth2没有此类接口:/auth/user。最后决定自写一个接口来替换原生: 123456@GetMapping(\"/api/v1/user\") public Authentication user(Map map, Principal user, Authentication auth) { //获取当前用户信息 logger.info(\"cas-server provide user: \" + JSON.toJSONString(auth)); return auth; } 在封装、覆盖后,在消费端直接配置相关配置: 123456789101112131415161718cas-server-url: http://cas-serversecurity: path: ignores: /,/index,/static/**,/css/**, /image/**, /favicon.ico, /js/**,/plugin/**,/avue.min.js,/img/**,/fonts/** oauth2: client: client-id: rest-service client-secret: rest-service-123 user-authorization-uri: ${cas-server-url}/oauth/authorize access-token-uri: ${cas-server-url}/oauth/token resource: loadBalanced: true id: rest-service prefer-token-info: false user-info-uri: ${cas-server-url}/api/v1/user authorization: check-token-access: ${cas-server-url}/oauth/check_token 同时启动认证中心、消费端,继续获取token后,请求接口: 此时,发现是403,没有权限了,这下我们可以对用户添加这种权限即可: 1\"authorities\": [ { \"authority\": \"ROLE_admin\" }, { \"authority\": \"admin\" } 添加完之后,我们发现可以请求接口成功: 12{ \"authorities\": [ { \"authority\": \"ROLE_admin\" }, { \"authority\": \"admin\" } ], \"details\": { \"remoteAddress\": \"0:0:0:0:0:0:0:1\", \"sessionId\": null, \"tokenValue\": \"b34841b4-61fa-4dbb-9e2b-76496deb27b4\", \"tokenType\": \"bearer\", \"decodedDetails\": null }, \"authenticated\": true, \"userAuthentication\": { \"authorities\": [ { \"authority\": \"ROLE_admin\" }, { \"authority\": \"admin\" } ], \"details\": { \"authorities\": [ { \"authority\": \"ROLE_admin\" }, { \"authority\": \"admin\" } ], \"details\": { \"remoteAddress\": \"169.254.200.12\", \"sessionId\": null, \"tokenValue\": \"b34841b4-61fa-4dbb-9e2b-76496deb27b4\", \"tokenType\": \"Bearer\", \"decodedDetails\": null }, \"authenticated\": true, \"userAuthentication\": { \"authorities\": [ { \"authority\": \"ROLE_admin\" }, { \"authority\": \"admin\" } ],... 这里简单测试,直接写的返回当前用户权限的接口,发现权限就是”ROLE_admin、”admin”。 总结有时候官网的源码解析很少,我们必须看源码,结合实际行动才能准确的分析其用意。所以当其不存在、或者不满足我们的需求时,可以选择覆盖其源码逻辑,实现自定义模式,这样会避免很多不必要的麻烦。因为源码解析毕竟不同版本,对应的源码也是不同的。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: https://gitee.com/damon_one/spring-cloud-k8s 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"Java","slug":"Java","permalink":"http://damon008.github.io/tags/Java/"}]},{"title":"基于 spring-cloud-k8s 跨NS坑续集","date":"2021-11-11T07:07:51.000Z","path":"2021/11/11/spring-cloud-k8s-note2/","text":"在前面文章 (spring-cloud-k8s 跨 NS 的坑) 中,讲述了 spring-cloud-k8s 中,如何利用 k8s 基于 Ribbon 等负载均衡利器来实现 LB,但存在跨命名空间的问题。 今天主要分享的是,基于 K8s 本身的 LB 利器,如何实现跨命名空间的应用服务互相访问,而且不是通过 K8s 原生的负载均衡 url 方式。还是基于 ServiceName。 直击源码首先,我们新建一个服务提供者:diff-ns-service,该服务提供了一个接口: 123456789/** * 返回远程调用的结果 * @return */@RequestMapping(\"/getservicedetail\")public String getservicedetail( @RequestParam(value = \"servicename\", defaultValue = \"\") String servicename) { return JSON.toJSONString(discoveryClient.getInstances(servicename));} 该接口的功能是返回指定 service 的相关信息。比如:这个 Service 对应的有几个 pod,每个 pod 的节点信息,host 等。 如果想结合 K8s 来实现这个服务的发现,可以基于这个配置: 123456789101112131415161718192021management: endpoint: restart: enabled: true health: enabled: true info: enabled: truespring: application: name: diff-ns-service cloud: loadbalancer: ribbon: enabled: false kubernetes: ribbon: mode: SERVICE discovery: all-namespaces: true 另外,如果想利用 k8s configMap 的配置来实现动态刷新应用服务的环境配置,可以这样配置: 12345678910111213141516spring: application: name: diff-ns-service cloud: kubernetes: reload: enabled: true strategy: refresh mode: event config: name: ${spring.application.name} namespace: default sources: - name: ${spring.application.name} namespace: ns-app 这里的动态刷新的模式有两个:[polling、event。一个是主动拉取,一个是当 configmap 发生改变时,这种事件会被监听到,会主动刷新。 另外,这个刷新的策略也有几种: refresh,直接刷新 restart_context,整个 Spring Context 会优雅重启,里面的所有配置都会重新加载 shutdown,重启容器 这样,我们再来配置一下 Service: 12345678910111213apiVersion: v1kind: Servicemetadata: name: diff-ns-service-service namespace: ns-appspec: type: NodePort ports: - name: diff-ns-svc port: 2008 targetPort: 2001 selector: app: diff-ns-service 这里我们设置了 Service 的 port,并且这个 Service 以 NodePort 类型创建。在(spring-cloud-k8s 跨 NS 的坑)一文中,我们使用的是默认的类型:ClusterIp。 这样,一个简单的服务提供者就创建成功了。接下来,我们看看服务消费者。 同样,我们先来创建一个服务 rest-service,创建接口: 1234567891011121314151617181920@GetMapping(\"/getClientRes\")public Response<Object> getClientRes() throws Exception { HttpHeaders headers = new HttpHeaders(); MediaType type = MediaType.parseMediaType(\"application/json; charset=UTF-8\"); headers.setContentType(type); headers.add(\"Accept\", MediaType.APPLICATION_JSON.toString()); HttpEntity<String> formEntity = new HttpEntity<String>(null, headers); String body = \"\"; try { ResponseEntity<String> responseEntity = restTemplate.exchange(\"http://diff-ns-service-service/getservicedetail?servicename=cas-server-service\", HttpMethod.GET, formEntity, String.class); System.out.println(JSON.toJSONString(responseEntity)); if (responseEntity.getStatusCodeValue() == 200) { return Response.ok(responseEntity.getBody()); } } catch (Exception e) { System.out.println(e.getMessage()); } return Response.error(\"failed\");} 同理地,结合 K8s 来实现这个服务的发现,可以基于这个配置: 123456789101112131415161718192021management: endpoint: restart: enabled: true health: enabled: true info: enabled: truespring: application: name: rest-service cloud: loadbalancer: ribbon: enabled: false kubernetes: ribbon: mode: SERVICE discovery: all-namespaces: true 这里,我们不使用 RibbonLoadBalancerClient。 另外,如果想利用 k8s configMap 的配置来实现动态刷新应用服务的环境配置,可以这样配置: 1234567891011121314spring: cloud: kubernetes: reload: enabled: true strategy: refresh mode: event config: name: ${spring.application.name} namespace: default sources: - name: ${spring.application.name} namespace: system-server 对于这些,在前面的文章说过,我们需要依赖配置: 123456789101112131415161718192021222324252627282930313233343536373839<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-actuator</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-actuator-autoconfigure</artifactId></dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-kubernetes</artifactId></dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-kubernetes-config</artifactId></dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-kubernetes-discovery</artifactId></dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-kubernetes-loadbalancer</artifactId></dependency><dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId></dependency> 这里没有用 Ribbon 的,直接使用 spring-cloud-starter-kubernetes-loadbalancer,但我们还是利用 RestTemplate: 123456789@LoadBalanced@Beanpublic RestTemplate restTemplate() { SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); requestFactory.setReadTimeout(env.getProperty(\"client.http.request.readTimeout\", Integer.class, 15000)); requestFactory.setConnectTimeout(env.getProperty(\"client.http.request.connectTimeout\", Integer.class, 3000)); RestTemplate rt = new RestTemplate(requestFactory); return rt;} 接下来,我们开始部署这两个应用服务了,同时,我们采用服务扩容方式实现多 pod: 1kubectl scale --replicas=2 deployment diff-ns-service-deployment 我们苦役看看服务节点信息: 查看 Service 信息: 接下来,我们访问服务rest-service:http://192.168.8.107:5556/rest-service/getClientRes, 这里我们可以看到日志: 同时,去哦们可以看到返回结果:这里,我们请求的是获取cas-server 这个服务的 pod 的分布信息。 同样地,我们通过 Service 的 Ip 和端口也可以直接访问:http://192.168.8.107:30916/getservicedetail?servicename=cas-server-service PS:如果需要实现负载均衡,还是需要注入:@LoadBalanced,如果我们把这个注解去掉会发生什么呢? 我们发现去掉后,竟然不能访问了。 我们再做一组测试,如果我们利用 spring.cloud.kubernetes.ribbon.mode=POD,我们来看看会有啥结果不?修改配置后,重新编译、部署,我们继续请求 urlhttp://192.168.8.107:5556/rest-service/getClientRes: 新发现如果我们引入的是基于 Spring cloud 本身的spring-cloud-starter-kubernetes-loadbalancer,同时,我们没有去掉基于 Ribbon 的 LB 的能力,如:spring.cloud.loadbalancer.ribbon.enabled=false,是有可能会报错的: 总结 Spring cloud 本身:如果是基于 Spring cloud 本身的 LB,需要隐藏 Ribbon 的能力,同时基于RestTemplate 需要注解@LoadBalanced。 k8s 本身:如果采用 Spring cloud 的负载,再结合 K8s,可以实现应用服务的 LB。如果设置spring.cloud.kubernetes.ribbon.mode=POD,其禁用了 Ribbon 的 LB 能力,此时不会生效,走的还是 Spring cloud LoadBalancer。另外对于 Service,这里都设置为 NodePort 类型,如果是默认类型是否可以实现 LB,需要待确认,因为目前来看,没有实现,可能是网络问题,并不是说默认类型的 Service 不可实现 LB。 Ribbon,基于上面,下次可以尝试基于 NodePort 类型的 Service 来实现 Ribbon 的 LB,看是否是因为 Service 的网络导致的。 实践验证在前面我们已经针对默认类型的Service进行Ribbon负载均衡测试过,发现无法对跨 NS 进行LB。接下来,我们测试下基于 NodePort 类型的Service,打开spring.cloud.loadbalancer.ribbon.enabled=true,引入依赖: 12345</dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-kubernetes-ribbon</artifactId></dependency> 按照以上配置,我们部署服务diff-ns-service,我们发现服务启动后日志: 请求后返回日志: 结论不管怎样,Ribbon 无法解决跨 NS 的应用服务之间的相互访问。但对于 Service 类型来说,可能是网络设置问题,跟其类型无关。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: https://gitee.com/damon_one/spring-cloud-k8s 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"后端","slug":"后端","permalink":"http://damon008.github.io/tags/%E5%90%8E%E7%AB%AF/"},{"name":"Java","slug":"Java","permalink":"http://damon008.github.io/tags/Java/"},{"name":"K8s","slug":"K8s","permalink":"http://damon008.github.io/tags/K8s/"}]},{"title":"spring-cloud-k8s 跨 NS 的坑","date":"2021-11-11T07:06:15.000Z","path":"2021/11/11/spring-cloud-k8s-note1/","text":"回顾前面文章 (Spring Cloud Kubernetes 之实战服务注册与发现) 中,讲述了 spring-cloud-k8s 在微服务实践中,带来了多大的优势。介绍了 k8s 中资源 Service,其如何来实现服务的注册与发现。 其实在 k8s 中,Service 资源的类型比较多,有四种: ExternalName:创建一个 DNS 别名指向 service name,这样可以防止 service name 发生变化,但需要配合 DNS 插件使用。 ClusterIP:默认的类型,用于为集群内 Pod 访问时,提供的固定访问地址,默认是自动分配地址,可使用 ClusterIP 关键字指定固定 IP。 NodePort:基于 ClusterIp,用于为集群外部访问 Service 后面 Pod 提供访问接入端口。 LoadBalancer:它是基于 NodePort。 我们一般会默认使用的类型:ClusterIP,但此时会出现一种问题,那就是此类型的 Service 被用来访问非同一 NS 下的 pods,即<servicename>.<namespace>.svc.cluster.local形式访问 pod,只能通过 servicename 直接访问同一 namespace 下的 pod。 案例下面,我们来看案例:假设我这里有三个服务:cas-server、rest-service、diff-ns-service 等,我通过 deployment 来部署这些服务的 pod。 可以看到这些 pod 处于 不同的 namespace 下,同样的对应的 service 也是处于对应的 namespace 下: 123ns-app diff-ns-service-service ClusterIP 10.16.178.187 <none> 2008/TCP 6h39m app=diff-ns-servicesystem-server cas-server-service ClusterIP 10.16.134.168 <none> 2000/TCP 16d app=cas-serversystem-server rest-service-service ClusterIP 10.16.128.58 <none> 2001/TCP 16d app=rest-service 这里的 Service 类型都是 ClusterIp,在前面,我们验证过基于这样的服务,我们可以利用 springcloud-k8s 来实现同一 namespace 下服务之间的注册与发现,实现负载均衡。但如果不在同一 namespace 下呢?比如这里的diff-ns-service,它与另外两个服务不在同一 namespace。此时我们通过基于 Ribbon 的负载均衡策略。这是因为我们默认了 KubernetesRibbonMode 的模式:POD,就是获取服务提供者的 pod 的 ip 和 port,该 ip 是 kubernetes 集群的内部 ip,只要服务消费者是部署在同一个 kubernetes 集群内就能通过 pod 的 ip 和服务提供者暴露的端口访问。当我们使用当mode为SERVICE时,就是获取服务提供者在 k8s 中的 service 的名称和端口,使用这种模式会导致 Ribbon 的负载均衡失效,转而使用 k8s 的负载均衡。 所以,如果不使用默认的 Ribbon 来实现负载均衡,可以配置: 12345spring: cloud: kubernetes: ribbon: mode: SERVICE 这个前提其实还是在同一 namespace 下,但如果不在同一 NS 呢?还是设置为SERVICE模式,但里面还是用 k8s 原生的调用方式:<servicename>.<namespace>.svc.cluster.local***,假设这里需要调用 diff-ns-service,则: 12ResponseEntity<String> responseEntity = new RestTemplate().exchange(\"http://diff-ns-service-service/getservicedetail?servicename=cas-server-service\", HttpMethod.GET, formEntity, String.class); 访问请求该服务时,发现并未请求到: 12342021-11-04 09:23:16.830:147 [http-nio2-2001-exec-2] DEBUG org.springframework.web.client.RestTemplate -HTTP GET http://diff-ns-service-service/getservicedetail?servicename=cas-server-service2021-11-04 09:23:16.834:147 [http-nio2-2001-exec-2] DEBUG org.springframework.web.client.RestTemplate -Accept=[text/plain, application/json, application/*+json, */*]2021-11-04 09:23:16.859:255 [http-nio2-2001-exec-2] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor -Using 'text/html', given [text/html, application/xhtml+xml, image/avif, image/webp, image/apng, application/xml;q=0.9, application/signed-exchange;v=b3;q=0.9, */*;q=0.8] and supported [text/plain, */*, text/plain, */*, application/json, application/*+json, application/json, application/*+json]2021-11-04 09:23:16.860:91 [http-nio2-2001-exec-2] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor -Writing [\"\"] 如果走 mode 为POD的 Ribbon 的负载均衡: 123456789102021-11-04 09:34:32.188:147 [http-nio2-2001-exec-2] DEBUG org.springframework.web.client.RestTemplate -HTTP GET http://diff-ns-service-service/getservicedetail?servicename=cas-server-service2021-11-04 09:34:32.193:147 [http-nio2-2001-exec-2] DEBUG org.springframework.web.client.RestTemplate -Accept=[text/plain, application/json, application/*+json, */*]2021-11-04 09:34:32.261:115 [http-nio2-2001-exec-2] INFO com.netflix.config.ChainedDynamicProperty -Flipping property: diff-ns-service-service.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 21474836472021-11-04 09:34:32.271:197 [http-nio2-2001-exec-2] INFO com.netflix.loadbalancer.BaseLoadBalancer -Client: diff-ns-service-service instantiated a LoadBalancer: DynamicServerListLoadBalancer:{NFLoadBalancer:name=diff-ns-service-service,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:null2021-11-04 09:34:32.278:222 [http-nio2-2001-exec-2] INFO com.netflix.loadbalancer.DynamicServerListLoadBalancer -Using serverListUpdater PollingServerListUpdater2021-11-04 09:34:32.281:88 [http-nio2-2001-exec-2] WARN org.springframework.cloud.kubernetes.ribbon.KubernetesEndpointsServerList -Did not find any endpoints in ribbon in namespace [system-server] for name [diff-ns-service-service] and portName [null]2021-11-04 09:34:32.282:150 [http-nio2-2001-exec-2] INFO com.netflix.loadbalancer.DynamicServerListLoadBalancer -DynamicServerListLoadBalancer for client diff-ns-service-service initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=diff-ns-service-service,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:org.springframework.cloud.kubernetes.ribbon.KubernetesEndpointsServerList@d48bf012021-11-04 09:34:32.308:255 [http-nio2-2001-exec-2] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor -Using 'text/html', given [text/html, application/xhtml+xml, image/avif, image/webp, image/apng, application/xml;q=0.9, application/signed-exchange;v=b3;q=0.9, */*;q=0.8] and supported [text/plain, */*, text/plain, */*, application/json, application/*+json, application/json, application/*+json]2021-11-04 09:34:32.309:91 [http-nio2-2001-exec-2] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor -Writing [\"\"]2021-11-04 09:34:32.316:1131 [http-nio2-2001-exec-2] DEBUG org.springframework.web.servlet.DispatcherServlet -Completed 200 OK 此时,给我的感觉就是: 12345678spring: cloud: kubernetes: ribbon: #直接走k8s的LB mode: SERVICE #POD走ribbon的LB discovery: all-namespaces: true 此类配置是无法进行 Service 到应用服务的访问,只能访问到 Service。同时我们看到日志: 12021-11-04 09:34:32.281:88 [http-nio2-2001-exec-2] WARN org.springframework.cloud.kubernetes.ribbon.KubernetesEndpointsServerList -Did not find any endpoints in ribbon in namespace [system-server] for name [diff-ns-service-service] and portName [null] 上面给到的是 mode 为 POD 时,走的 Ribbon 的负载均衡后,无法找到当前 pod 对应的 NS 下的 Servcie 为 diff-ns-service-service 的服务。 12021-11-04 09:20:27.109:89 [PollingServerListUpdater-1] WARN org.springframework.cloud.kubernetes.ribbon.KubernetesServicesServerList -Did not find any service in ribbon in namespace [system-server] for name [diff-ns-service-service] and portName [null] 同样地,当 mode 为 SERVICE 时,依然无法找到当前 pod 的对应的 NS 的 Servcie 为 diff-ns-service-service 的服务。 同样会拿不到请求返回信息,这里说明:在不同NS下,Service为ClusterIP,不管如何负载均衡,都无法访问。 解决方案一、通过 Springcloud k8s 社区来实现跨 NS 下的服务的相互访问的简单策略二、走 K8s 的原生的负载均衡策略从前面的分析可以看到:虽然 spring-cloud-k8s 帮我们发现了 Service,但在底层策略时,不同的NS还是做了隔离,只能通过 k8s 原生的方式来进行服务的发现:<servicename>.<namespace>.svc.cluster.local PS:同时,我们需要注意的是,此时基于 k8s 负载均衡,我们不能再基于 Ribbon 或其他来进行负载均衡机制了,直接通过 Http 协议来请求 k8s 的 service,实现跨 NS 的 pod 之间的互通。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: https://gitee.com/damon_one/spring-cloud-k8s 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"后端","slug":"后端","permalink":"http://damon008.github.io/tags/%E5%90%8E%E7%AB%AF/"},{"name":"Java","slug":"Java","permalink":"http://damon008.github.io/tags/Java/"},{"name":"K8s","slug":"K8s","permalink":"http://damon008.github.io/tags/K8s/"}]},{"title":"Springcloud Oauth2 HA篇","date":"2021-10-11T07:17:58.000Z","path":"2021/10/11/oauth-ha/","text":"在 浅谈微服务安全架构设计 一文中,介绍了基于Springcloud 结合了Oauth2分析了其各种模式下的鉴权认证,今天主要分析如何结合k8s作实现鉴权的高可用。 假设我们的项目中有几个模块: 鉴权中心:Oauth2服务 订单系统:客户端A 用户管理系统:客户端B 在上面的系统中,每个服务之间的耦合性很低,但是又有着很频繁的调用,这就涉及到UI与其之间的频繁流量交互。如何做到其HA,这里引入k8s的Service方法: 在 Spring Cloud Kubernetes之实战服务注册与发现一文中,就讲解了k8s的Service方式创建服务,然后可以部署多个pod,同时结合 Spring Cloud Kubernetes之实战网关Gateway 来实现LB,类似通过域名来解析其服务,并根据所定义的规则进行LB。同样,本文则是Oauth2的基础上,结合这些来实现微服务的LB。同时此处利用了k8s来作主要处理,如果是其他语言(Python、Go、Rust等)的客户端服务,则自身可以通过逻辑来控制其鉴权以及获取流量的。 注意点:由于各微服务与鉴权中心有交互,故鉴权中心需要提供HA服务,即先在启动类加入@EnableDiscoveryClient ,后续在注入bean时,@LoadBalanced来实现LB鉴权中心。 12345678910111213@EnableOAuth2Sso@Configuration@EnableAutoConfiguration@ComponentScan(basePackages = {\"com.damon\"})@EnableConfigurationProperties(EnvConfig.class)@EnableDiscoveryClient #为LB多节点鉴权中心准备public class AdminApp { public static void main(String[] args) { SpringApplication.run(AdminApp.class, args); }} 在客户端项目模块中,调用鉴权中心时,需要实现LB: 1234567891011121314151617@Configurationpublic class BeansConfig { @Resource private Environment env; @LoadBalanced @Bean public RestTemplate restTemplate() { SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); requestFactory.setReadTimeout(env.getProperty(\"client.http.request.readTimeout\", Integer.class, 15000)); requestFactory.setConnectTimeout(env.getProperty(\"client.http.request.connectTimeout\", Integer.class, 3000)); RestTemplate rt = new RestTemplate(requestFactory); return rt; }} 另外本身在配置交互的时候,需要加上域名等形式来实现LB,这里利用了k8s的Service来实现。 1234567891011121314cas-server-url: http://cas-server-service #这里配置成HA地址security: oauth2: #与cas-server对应的配置 client: client-id: admin-web client-secret: admin-web-123 user-authorization-uri: ${cas-server-url}/oauth/authorize #是授权码认证方式需要的 access-token-uri: ${cas-server-url}/oauth/token #是密码模式需要用到的获取 token 的接口 resource: loadBalanced: true id: admin-web user-info-uri: ${cas-server-url}/api/user #指定user info的URI prefer-token-info: false 这样,一个客户端关于鉴权的核心就是如此了,同样需要把消费客户端以service形式提供给UI,此时需要借助 Spring Cloud Kubernetes之实战网关Gateway 和nginx代理服务,我们来测试下:curl -X POST -d \"username=admin&password=123456&grant_type=password&client_id=admin-web&client_secret=admin-web-123\" http://192.168.8.10:5556/cas-server/oauth/token 看到结果: 1{\"access_token\":\"5a7892b0-7483-4f60-89fd-44255a429ff6\",\"token_type\":\"bearer\",\"refresh_token\":\"23f2e8ea-f091-4ab0-822c-f28bebc4ec08\",\"expires_in\":3599,\"scope\":\"all\"} 通过获取到的access_token来访问对应的客户端:curl -H \"Accept: application/json\" -H \"Authorization:bearer 5a7892b0-7483-4f60-89fd-44255a429ff6\" -X GET http://192.168.8.10:5556/admin-web/api/user/getCurrentUser 输出结果: 1{\"authorities\":[{\"authority\":\"admin\"}],\"details\":{\"remoteAddress\":\"10.244.0.196\",\"sessionId\":null,\"tokenValue\":\"5a7892b0-7483-4f60-89fd-44255a429ff6\",\"tokenType\":\"bearer\",\"decodedDetails\":null},\"authenticated\":true,\"userAuthentication\":{\"authorities\":[{\"authority\":\"admin\"}],\"details\":{\"authorities\":[{\"authority\":\"admin\"}],\"details\":{\"remoteAddress\":\"10.244.0.201\",\"sessionId\":null,\"tokenValue\":\"5a7892b0-7483-4f60-89fd-44255a429ff6\",\"tokenType\":\"Bearer\",\"decodedDetails\":null},\"authenticated\":true,\"userAuthentication\":{\"authorities\":[{\"authority\":\"admin\"}],\"details\":{\"client_secret\":\"admin-web-123\",\"grant_type\":\"password\",\"client_id\":\"admin-web\",\"username\":\"admin\"},\"authenticated\":true,\"principal\":{\"password\":null,\"username\":\"admin\",\"authorities\":[{\"authority\":\"admin\"}],\"accountNonExpired\":true,\"accountNonLocked\":true,\"credentialsNonExpired\":true,\"enabled\":true},\"credentials\":null,\"name\":\"admin\"},\"oauth2Request\":{\"clientId\":\"admin-web\",\"scope\":[\"all\"],\"requestParameters\":{\"grant_type\":\"password\",\"client_id\":\"admin-web\",\"username\":\"admin\"},\"resourceIds\":[],\"authorities\":[],\"approved\":true,\"refresh\":false,\"redirectUri\":null,\"responseTypes\":[],\"extensions\":{},\"grantType\":\"password\",\"refreshTokenRequest\":null}……… 最后,这里鉴权的高可用通过k8s的service,进行默认的轮询方式的访问鉴权中心,鉴权中心如果鉴权时不管使用redis还是jwt,来管理token,都是可以的。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: https://gitee.com/damon_one/spring-cloud-k8s 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"Java","slug":"Java","permalink":"http://damon008.github.io/tags/Java/"}]},{"title":"Jupyter Notebook 的使用","date":"2021-07-12T09:45:47.000Z","path":"2021/07/12/jupyter/","text":"深度学习编程常用工具我们先来看 4 个常用的编程工具:Sublime Text、Vim、Jupyter。虽然我介绍的是 Jupyter,但并不是要求你必须使用它,你也可以根据自己的喜好自由选择。 Sublime Text第一个是 Sublime Text,它是一个非常轻量且强大的文本编辑工具,内置了很多快捷的功能,同时还支持很丰富的插件功能,对我们来说非常方便。 如上图所示,它可以自动为项目中的类、方法和函数生成索引,我们让我们可以跟踪代码。可以通过它的 goto anything 功能,根据一些关键字查找到项目中的对应的代码行。 Vim第二个是 Vim,它是 Linux 系统中的文本编辑工具,方便快捷且强大,我们在项目中经常会使用到。 在我们的项目中,经常需要登录到服务器上进行开发,而服务器一般都是 Linux 系统,不会有 Sublime Text 与 Pycharm,所以我们可以直接用 Vim 打开代码进行编辑。对于没有接触过 Linux 或者是一直使用 IDE 进行编程开发的同学,一开始可能觉得不是很方便,但 Vim 的快捷键十分丰富,对于 Shell 与 Python 的开发来说非常便捷。 Vim 的缺点正如刚才所说,有一点点门槛,需要你去学习它的使用方法。只要你学会了,我保证你将对它爱不释手。 Jupyter Notebook & Lab最后一个是今天要介绍的 Jupyter Notebook 了,它是一个开源的 Web 应用,能够让你创建、分享包含可执行代码、可视化结构和文字说明的文档。 Jupyter Notebook 的应用非常广泛,它可以用在数据清理与转换、数字模拟、统计模型、数据可视化、机器学习等方面。 Jupyter Notebooks 非常活跃于深度学习领域。在项目的实验测试阶段,它相比于用 py 文件来直接编程还是方便一些。在项目结束之后如果要写项目报告,用 Jupyter 也比较合适。 简单介绍之后,我们接下来就从 Jupyter 的功能、Jupyter 的安装与启动与 Jupyter Lab 的操作这 3 个方面学习 Jupyter。 Jupyter Notebook & Lab 的功能Jupyter 主要有以下 3 点的作用:执行代码、数据可视化以及使用 Markdown 功能写报告。 执行代码。一般是 Python 程序,也可以添加新的编程语言。 数据可视化。设想一下,我们经常在 Linux 环境编程开发,如果需要对数据可视化该怎么办呢?是不是只能把图片保存下来,然后下载到本地进行查看?使用 Jupyter Notebook 就不用多此一举,我们可以直接在页面中查看。如下图所示: 使用 Markdown 功能写文档,或者制作 PPT。这些文档中还包含代码以及代码执行后的结果,非常有助于你书写项目报告。 Jupyter Notebook & Lab 的安装与启动了解了 Jupyter 的功能之后,我们来看看具体要如何进行安装与启动。这一节我介绍了 3 种安装和启动的方式,分别是 Anaconda、Docker 和 pip。 使用 Anaconda 安装与启动我们先来看如何使用 Anaconda 来安装与启动。 安装最简单的方法是通过安装 Anaconda 来使用 Jupyter Notebook & Lab。Anaconda 已自动安装了 Jupter Notebook 及其他工具,还有 Python 中超过 180 个科学包及其依赖项。你可以通过 Anaconda 的官方网站得到 Anaconda 的下载工具。 启动这里我会分 MacOS 系统和 Win 环境来讲解。 (1)MacOS 系统 安装完 Anaconda 之后,打开终端后系统会默认进入 base 环境。 在命令行最前面有个**(base)**的标志则表示代码进入 base 环境了,如果没有就需要通过下面的命令激活 base 环境: 1conda activate base 在 base 环境下执行下面的命令,会自动进入 Jupyte Notebook 的开发环境。 1jupyter notebook 执行下面的命令,则会自动进入到 Jupyter Lab 的开发环境。 1jupyter lab (2)Win 环境 Windows 环境中的启动方式与 MacOS 基本一样。 当你想通过命令 Jupyter Notebook 或 Jupyter Lab 启动时,你需要在 Anaconda Prompt 中执行。 通过 Anaconda Navigator 启动的方式与 MacOS 一样。 使用 Docker通过 Docker 使用 Jupyter 也非常简单,连安装都不需要,但前提是你要有 Docker 相关的知识。 使用 pip 安装与启动了解完 Anaconda 和 Docker 的安装与启动方式后,我们最后来看 pip 是如何安装和启动的。 安装通过 pip 安装 Jupyter Notebook: 1pip install Jupyter 通过 pip 安装 Jupyter Lab: 1pip install Jupyterlab 启动安装完成后,直接在终端执行 Jupyter Notebok 或 Jupyter Lab 命令启动。 不管在 MacOS 系统还是在 Windows 系统,通过以上任意一种方式成功启动后,浏览器都会自动打开 Jupyter Notebook 或 Jupyter Lab 的开发环境: Jupyter Lab 的操作Jupyter Lab 是 Jupyter Notebook 的下一代产品,在使用方式上更为灵活、便捷。 我们在命令行或者 Anaconda Navigator 中启动 Jupyter Lab 之后,浏览器会自动打开如下所示的 Jupyter Lab 界面: 最左侧显示的是你启动时所在的目录,右侧是你可以使用的一些开发工具。 Notebook点击 Notebook 下面的“Python 3”的图标之后,就会自动新建一个 Notebook。 Jypter Lab 与 Jupyter Notebook 中都会用到这个叫作 Notebook 的编辑工具。 Jupyter Lab 与 Jupyter Notebook 不同的地方是 IDE 的界面以及操作方式,这里讲解用的是 Jupyter Lab 的操作。 一个 Notebook 的编辑界面主要由 4 个部分组成:菜单栏、工具栏、单元格(Cell)以及内核。如下图所示: 菜单栏与工具栏这里就不详细介绍了。我们先来看单元格(Cell),然后再介绍内核。 单元格(Cell)单元格是我们 Notebook 的主要内容,这里我会介绍两种单元格。 Code 单元格:包含可以在内核运行的代码,并且在单元格下方输出运行结果。 Markdown 单元格:包含运用 Markdown 的文档,常用于文档的说明,也是可以运行的单元格。 从 Code 单元格切换到 Markdown 单元格的切换的快捷键是 m;从 Markdown 单元格切换到 Code 单元格的切换的快捷键是 y。 切换之前需要先按 Esc,从单元格的编辑状态中退出。 在工具栏中也可以切换,但是还是快捷键方便些。工具栏的位置在下图中红框的位置: 我们看一个例子。我编辑了下面的 Notebook。第一行是 1 个 Markdown 单元格,是 1 个一级标题,第二行是 1 个 Python 的代码。两行代码都是未运行状态。 你注意到左边那个蓝色的竖条了吗?它代表我们所在的单元格。 我们在编辑这个单元格的时候,左边是绿色的竖条。如果我们按 Esc 退出单元格,它就会变为蓝色。 退出单元格后,我们可以通过上下键移动选中的单元格。我们移动到第一行,然后开始运行这两个单元格。 运行单独一个单元格的快捷键 Ctrl+Enter,运行选中单元格并切换到下一个单元格的快捷键是 Shift + Enter。运行结果如下图所示: Markdown 没有左边的“[]”标签,通过这一点你可以区分 Code 单元格与 Markdown 单元格。 “[]”中的数字代表单元格被执行的顺序,例子中“[1]”代表第一个被执行的单元格。 以上就是单元格的内容了。我们接下来看看,单元格中的一些快捷键的使用。 (1)快捷键 如果你是用 Jupyter 进行开发,掌握单元格的快捷键能让你的开发速度变得更快,下面我列举了几个常用的快捷键: 执行单元格 Ctrl+Enter 或 Shift+Enter; a 在单元格上方插入新的单元格; b 在单元格下方插入新的单元格; x 删除单元格; z 撤销删除的单元格。 (2)Magic 命令 Jupyter Notebook 的前身是 IPython Notebook,所以 Jupyter 也支持 IPython 的 Magic 命令。IPython 是一个比 Python 自带的 Shell 更加灵活方便的 Shell,它主要活跃于数据科学领域。 Magic 命令分两种: Line Magics 命令:在命令前面加%,表示只在本行有效 Cell Magics 命令:在命令前面加%%,表示在整个 Cell 单元有效。 下面我介绍几个常用的 Magic 命令。 %lsmagic:用来查看可以使用的 Magic 命令。 %matplotlib inline:可以在单元格下面直接打印出 matplotlib 的图标,通常要在 matplotlib 模块引入之前使用;使用这个 Magic 命令之后,可以不用 plt.show()。 %pwd:查看当前的文件路径。 %%writefile:写文件,%%writefile 后面紧跟着文件名,然后下面写文件的内容。 %run: 运行一个文件,%run 后面跟着要运行的文件。 %load:加载文件。使用%load + 文件名可以把指定的文件加载到单元格内。请看下面的例子,我们要把 temp.py 加载到单元格里,首先是执行前, (3)Markdown 命令 了解了 Magic 命令后,我们再来看 Markdown 命令。Markdown 是一种在 Markdown 单元中用于格式化文本的语言,常用于 Notebook 的文档说明,我们列举了几个常用的命令。 标题:通过井号的数目可以决定标题的大小。 123456789# 一级标题:## 二级标题:### 三级标题:#### 四级标题:##### 五级标题: 列表:分为无序列表与有序列表。 1234567891011## 无序列表- 项目 1- 项目 2## 有序列表1. 项目 1 (1. 与项目 1 之间有一个空格)2. 项目 2 字体:可以通过”*”或者_的数目控制强调的内容,即斜体、加粗以及粗斜体。具体的请看下面的例子。12345678910111213*斜体***加粗*****粗斜体***或者_斜体___加粗_____粗斜体___ (4)调用系统命令 最后,在 Notebook 中还可以调用所在操作系统的命令,只需要在命令前加一个“!”就可以了。例如,在 Linux 系统中查看当前路径: 1!pwd","tags":[{"name":"Jupyter","slug":"Jupyter","permalink":"http://damon008.github.io/tags/Jupyter/"}]},{"title":"Service Mesh 是下一代微服务架构","date":"2021-03-30T07:09:35.000Z","path":"2021/03/30/ServiceMesh-01/","text":"Service Mesh 背后的诉求一种技术的出现必然是有各种推动的因素,Service Mesh 也一样,它的出现就得益于微服务架构的发展。那 Service Mesh 出现时,其背后的诉求是什么呢? 1. 微服务架构的复杂性在微服务架构中,应用系统往往被拆分成很多个微服务(可以多达成百上千),数量庞大的微服务实例使得服务治理具有一定的挑战,比如说常见的服务注册、服务发现、服务实例的负载均衡,以及为了保护服务器实现熔断、重试等基础功能。除此之外,应用程序中还加上了大量的非功能性代码。 归根结底,在微服务架构中,微服务组件复杂、上手门槛比较高成了痛点问题。业务开发团队需要一定的学习周期才能上手微服务架构的开发,而人力资源的昂贵以及人员的流动性使得开发成本变高。业务开发团队更加擅长的是某一具体领域的业务,而不是技术的深度。应用系统的核心价值在于实现相应的业务,所以对于业务开发人员来说,微服务仅仅是手段,不是最终的目标。我们需要对业务开发人员“屏蔽”微服务的基础组件,使得微服务之间的通信对于业务开发人员透明。 为应对这个问题,有一些实践是利用 API 网关接收请求,网关作为代理处理外部服务的请求,并提供服务注册与发现、负载均衡、日志监控、容错等功能。然而,这种方案也存在不足,比如网关的单点故障、系统架构变得异常庞大;从功能来看,API 网关主要是面向用户,也就是说它可以解决从用户到各个后端服务的流量问题,至于其他问题,它可能就无能为力了。而我们需要的是一个完整的贯穿整个请求周期的方案,或者至少是一些能够与 API 网关互补的方案和工具。 2. 微服务本身的挑战微服务还有其自身引入的复杂度,有比学习微服务框架更艰巨的挑战,如微服务的划分、设计良好的声明式 API、单体旧应用的迁移,还涉及跨多个服务的数据一致性,这都会令大部分团队疲于应付。 除此之外,版本兼容性也是一个挑战。微服务框架很难一开始就完美无缺,在现实的软件工程中一般不存在这样完美无缺的框架,功能会分为多个里程碑迭代,发布之后就会有补丁修复……没有任何问题,这只是一种理想状态。业务服务中引入微服务的基础组件,这样业务服务的代码和微服务的 SDK 强耦合在一起,导致业务升级和微服务 SDK 的升级强绑定在了一起。如果客户端 SDK 和服务器端版本不一致,那就得谨慎对待客户端与服务端的兼容性问题。版本兼容性的处理非常复杂,特别是在服务端和客户端数量庞大的情况下,每对客户端和服务端的版本都有可能不同,这对于兼容性测试也会造成很大的压力。同时,对于异构的系统,还需要开发多语言的 SDK,维护成本很高。 3. 本质接下来我们探讨下业务服务最关心的是什么,比如写一个商品服务,对商品做增删改查的操作,你会发现基础设施、跨语言、兼容性和商品服务本身并没关系,而服务间的通讯才是最需要解决的问题。 比如,为了保证将客户端发出的业务请求发去一个正确的地方,需要用什么样的负载均衡?要不要做灰度?最终这些解决方案,都是让请求去访问正确的后端服务。整个过程当中,这个请求是从来不发生更改的。 既然在开发微服务的时候不用特别关心服务的通讯层,那是不是可以把微服务的技术栈向下移呢? 微服务的早期先驱,如 Netflix、Twitter 等大型互联网公司,它们通过建立内部库的方式处理这些问题,然后提供给所有服务使用。但这种方法的问题在于这些库相对来说是比较“脆弱”的,很难保证它们可以适应所有的技术堆栈选择,且很难把库扩展到成百上千个微服务中。 为了应对上述的问题,Service Mesh 出现了, Service Mesh 通过独立进程的方式隔离微服务基础组件,对这个独立进程升级、运维要比传统的微服务方式简单得多。 什么是 Service MeshService Mesh(服务网格),最早在 2016 年 9 月,由开发 Linkerd 的 Buoyant 公司提出。2017 年,Linkerd 加入 CNCF,由 CNCF 托管孵化,Linkerd 是第一个加入 CNCF 的 Service Mesh 项目。Service Mesh 开始变得流行起来,特别是在技术社区,有人指出 Service Mesh 会是下一代的微服务架构基础。 关于 Service Mesh 的定义,目前比较认同的是 Buoyant 的 CEO William Morgan 在博客中给出的定义: 1Service Mesh 是用于处理服务到服务通信的专用基础架构层。云原生有着复杂的服务拓扑,它负责可靠的传递请求。实际上,Service Mesh 通常是作为一组轻量级网络代理实现,这些代理与应用程序代码部署在一起,应用程序无感知。 Service Mesh 模式的核心在于将客户端 SDK 剥离,以 Proxy 独立进程运行,目标是将原来存在于 SDK 中的各种能力下沉,为应用减负,以帮助应用云原生化。 Service Mesh 的第一代产品,如 Linkerd 1 和 Envoy,天然支持虚拟机。随着云原生的崛起,到了 Istio 和 Linkerd 2 ,不支持虚拟机。相比虚拟机,Kubernetes 提供了太多便利。 绝大部分Service Mesh 的实现都支持 Kubernetes,有些实现甚至只支持 Kubernetes。就这样,Service Mesh 逐步发展为一个独立的基础设施层。 在云原生架构下,应用系统可能由数百个微服务组成,微服务一般又是多实例部署,并且每一个实例都可能处于不断变化的状态,因为它们是由 Kubernetes 之类的资源调度系统动态调度。 Kubernetes 中的 Service Mesh 实现模式被命名为 Sidecar(边车模式,因为类似连接到摩托车的边车)。 在模式库中,Sidecar 模式的定义是:将应用程序的组件部署到单独的进程或容器中以提供隔离和封装。这种模式还可以使应用程序由异构组件和技术组成。 在 Sidecar 模式中,“边车”与父应用程序(即业务服务)是两个独立的进程,二者生命周期相同,同时被创建和退出。“边车”附加到业务服务,并为应用提供支持功能。 业务所有的流量都转发到 Service Mesh 的代理服务 Sidecar 中,Sidecar 承担了微服务框架基础的功能,包括服务注册与发现、负载均衡、熔断限流、认证鉴权、日志、监控和缓存加速等。不同的是,Service Mesh 强调的是通过独立进程的代理方式。总体来说,Service Mesh 帮助应用程序在复杂的软件架构和网络中建立稳定的通信机制。 Service Mesh 的开源组件近几年 Service Mesh 社区比较活跃,其对应的开源组件也很丰富,从最早的 Linkerd 到当前火热的 Istio、Envoy 等组件,下面我们就来重点介绍下这三个开源组件。 1. IstioIstio 由 Google、IBM 和 Lyft 合作开源,所以 Istio 自诞生之日起就备受瞩目。在 Istio 中,直接使用了 Lyft 公司的 Envoy 作为 Sidecar。2017 年 5 月 Istio 发布了 0.1 版本,现在已经发展到 1.9 版本。Istio 是 Service Mesh 的第二代产品,在刚开始发布时还曾计划提供对非 Kubernetes 的支持,发展到现在基本只支持 Kubernetes 上的使用,实质性取消了对虚拟机的支持。 Istio 功能十分丰富,包括: 流量管理:Istio 的基本功能,Istio 的流量路由规则使得你可以轻松控制服务之间的流量和 API 调用。 策略控制:应用策略并确保其得到执行,并且资源在消费者之间公平分配。 可观测性:通过自动链路追踪、监控和服务的日志,可以全面了解受监视服务如何与其他服务以及 Istio 组件本身进行交互。 安全认证:通过托管的身份验证,授权和服务之间通信的加密自动保护服务。Istio Security 提供了全面的安全解决方案来解决这些问题。 Istio 针对现有的服务网络,提供了一种简单的方式将连接、安全、控制和观测的模块,与应用程序或服务隔离开来,从而使开发人员可以将更多的精力放在核心的业务逻辑上。另外,Istio 直接基于成熟的 Envoy 代理进行构建,控制面组件则都是使用 Go 编写,在不侵入应用程序代码的前提下实现可视性与控制能力。总之,Istio 的设计理念是非常新颖前卫的。 2. Linkerd2016 年 1 月,前 Twitter 工程师 William Morgan 和 Oliver Gould 组建了一个名为 Buoyant 的公司,同时在 GitHub 上发布了 Linkerd 0.0.7 版本。Linkerd 由 Buoyant 推出,使用 Scala 语言实现,是业界第一个 Service Mesh。2017 年 1 月,Linkerd 加入 CNCF; 4 月,发布了 1.0 版本。 Linkerd 的架构由两部分组成:数据平面和控制平面。其中,数据平面由轻量级代理组成,它们作为 Sidecar 容器与服务代码的每个实例一起部署;控制平面是一组在专用 Kubernetes 命名空间中运行的服务(默认情况下)。这些服务承担聚合遥测数据、提供面向用户的 API、向数据平面代理提供控制数据等功能,它们共同驱动着数据平面的行为。 Linkerd 作为 Service Mesh 的先驱开源组件,在生产环境得到了大规模使用。Linkerd 2 的定位是 Kubernetes 的 Service Mesh,其提供了运行时调试、可观察性、可靠性和安全性,使得运行服务变得更容易、更安全,而无须更改代码。但是随着 Istio 的诞生,前景并不是特别乐观。 3. Envoy2016 年 9 月,Lyft 公司开源 Envoy ,并在 GitHub 上发布了 1.0.0 版本。Envoy 由 C++ 实现,性能和资源消耗上表现优秀。2017 年 9 月,Envoy 加入 CNCF,成为继 Linkerd 之后的第二个 Service Mesh 项目。Envoy 发展平稳,被 Istio 收编之后,Envoy 将自身定义为数据平面,并希望使用者可以通过控制平面来为 Envoy 提供动态配置。Envoy 用于云原生应用,为应用服务提供高性能分布式代理,以及作为大规模微服务架构的 Service Mesh 通信总线和通用数据平面。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"后端","slug":"后端","permalink":"http://damon008.github.io/tags/%E5%90%8E%E7%AB%AF/"}]},{"title":"什么是服务网格(Service Mesh)","date":"2021-03-30T06:41:28.000Z","path":"2021/03/30/Service-Mesh/","text":"服务网格(Service Mesh)是随着 Kubernetes 和微服务架构的流行而出现的新技术,它的目的是解决微服务架构的服务之间相互调用时可能存在的各种问题。微服务架构的服务之间采用进程间的通讯方式进行交互,比如 REST 或 gRPC 等。在第 01 课时介绍微服务架构的时候,我提到过影响微服务架构复杂度的一个重要因素就是微服务之间的相互调用,这使得应用需要对服务调用时产生的错误进行处理。比如,当调用一个服务出现超时错误时,应该进行重试;如果对某个服务的调用在一段时间内频繁出错,说明该服务可能已经崩溃或是负载过大,没有必要再继续进行尝试下去了。 除了错误处理之外,我们还可能需要对服务之间的调用添加一些策略,比如限制服务被调用的速率,或是添加安全相关的访问控制规则等。这些需求从服务之间的调用而来,并且所有微服务架构的应用都有同样的需求,这些横切的需求,应该由平台或工具来处理,而不需要应用来实现,应用要做的只是提供相关的配置即可。 在 Kubernetes 出现之前,微服务架构已经在很多企业内部得到了应用。同样的,在服务网格之前也有相似的工具来解决服务调用相关的问题,比如 Netflix OSS 栈中的 Hystrix,但服务网格技术是在已有工具上的升级,它提供了一个更完整的解决方案。 严格说来,服务网格并不直接依赖 Kubernetes,但绝大部分服务网格实现都支持 Kubernetes,有些实现甚至只支持 Kubernetes。这是因为 Kubernetes 平台提供的功能可以简化服务网格的使用。下面我来为你介绍 Kubernetes 中的边车模式(Sidecar)。 边车模式在 Kubernetes 中,Pod 中的容器通常是紧密耦合的,它们共同完成应用的功能。如果需要实现横切功能,则需要在 Pod 中添加与应用无关的容器,这是因为横切功能的实现离不开对应用使用的存储和网络的访问,而 Pod 中的容器之间共享存储和网络。当我们把横切服务的容器添加到 Pod 中后,Pod中就多了与应用无关的容器,这种部署模式称为边车模式,这些容器被称为边车容器,下图是现实世界中的边车。 日志收集是边车模式的一个常见应用,它利用了 Pod 中容器共享存储的特性:应用容器往某个持久卷中写入日志,而日志收集工具的边车容器则监控同一个持久卷中的文件来读取日志。 边车容器在服务网格实现中至关重要。服务网格实现会在每个 Pod 上增加一个新的边车容器来作为其中应用服务的代理,这个容器的代理程序会作为外部调用者和实际服务提供者之间的桥梁。 如下图所示,Pod 某个端口上的请求,首先会被服务代理处理,然后再转发给实际的应用服务;同样的,应用服务对外的请求,也会先被服务代理处理,然后再转发给实际的接收者。代理边车容器的出现,为解决服务调用相关的问题提供了一种新的方案:服务调用的自动重试和断路器模式的实现,都可以由服务代理来完成,从而简化应用服务的实现。 如果仅从最基本的实现方式上来说,服务网格技术并不复杂。打个比方,如果一个 Pod 提供某个应用服务,只需要在该 Pod 中部署一个服务代理的边车容器,由该代理来处理应用容器发送和接收的数据,就实现了服务网格。 但是,服务网格实际上的解决方案非常复杂,我会在下面进行具体的介绍。 值得一提的是,边车模式并不是服务代理的唯一部署方式。有些服务网格实现可以在Kubernetes的节点上部署服务代理来处理该节点上的全部请求。 服务代理服务代理是服务网格技术实现的核心,可以说,服务代理决定了服务网格能力的上限。从作用上来说,服务代理与我们所熟悉的 Nginx 和 HAProxy 这类代理并没有太大区别。实际上, Nginx 和 HAProxy 同样可以作为服务代理来使用,但服务网格通常使用专门为服务间调用开发的服务代理实现。在下图所示的 OSI 七层模型中,服务代理一般工作在第 3/4 层和第 7 层。 下表列出了常见的服务代理,其中 Envoy、Traefix 和 Linkerd 2 都是新出现的服务代理实现。 服务发出和接收的所有调用都需要经过服务代理。服务代理的功能都与服务之间的调用相关,其主要方面如下表所示。 代理可以在请求层上工作。当服务 A 调用服务 B 时,服务 A 的代理可以使用负载均衡来动态选择实际调用的服务 B 实例,如果对服务 B 的调用失败,并且该调用是幂等的,则代理可以自动进行重试。服务 A 的代理还可以记录与调用相关的指标数据,服务 B 的代理可以根据访问控制的策略决定是否允许该请求,如果服务 B 当前所接收的请求过多,那么它的代理可以拒绝其中某些请求。 代理同样可以工作在连接层,服务 A 和服务 B 的代理之间可以建立 TLS 连接,并验证对方的身份。 由于服务代理需要处理服务所有接收和发送的请求,这对服务代理的性能要求很高,不能增加过长的延迟,这也是 Envoy 等服务代理流行的原因,这些新开发的服务代理对服务之间的调用进行了优化。除了性能之外,服务代理只占用很少的 CPU 和内存资源,这是因为每个服务实例的 Pod 上都可能运行着一个服务代理的容器,当服务数量增加时,服务代理自身的资源开销也会增加。 服务网格服务网格技术起源于 Linkerd 项目,从架构上来说,服务网格的实现很简单,它由服务代理和管理进程组成。服务代理称为服务网格的数据平面(Data Plane),负责拦截服务之间的调用并进行处理;管理进程称为服务网格的控制平面(Control Plane),负责协调代理并提供 API 来管理和监控服务网格。服务网格的能力由这两个平面的能力共同决定。 下图给出了服务网格的基本架构: 服务网格在数据平面的处理能力取决于所使用的服务代理,而服务网格实现通常使用已有的服务代理,因此它们在数据平面方面的能力差别并不大。服务网格实现的价值更多来源于它所提供的控制平面,比如,服务网格实现是否提供了 API 来更新配置,是否提供了图形化界面来查看服务状态,在 Kubernetes 上,是否可以使用自定义资源定义(Custom Resource Definition,CRD)来进行声明式配置。 服务网格技术的优势有以下几个方面: 它与服务实现使用的技术栈无关。服务代理工作在服务调用这个层次上。不论服务采用什么编程语言或框架来实现,服务代理都可以产生作用。Kubernetes 的流行,使得在微服务架构实现中使用多语言开发变得更简单。一个微服务应用的不同服务可以使用完全不同的技术栈来实现,这些服务之间的调用都可以由服务代理来处理。 服务网格技术与应用代码是解耦的,这意味着当我们需要对服务调用相关的策略进行调整时,并不需要修改应用的代码。以服务的访问频率为例,当需要控制对某个服务的调用频率时,可以通过服务网格的控制平面提供的 API 直接进行修改,并不需要对应用做任何改动。这种解耦使得服务网格成为应用运行平台所提供的能力之一,进而促成了新的开源项目和商业产品的出现。 对于大型项目,可以由专门的团队来负责管理服务网格的配置,进行更新和日常维护;对于小型项目,可以从开源社区选择合适的产品。 服务网格功能服务网格所能提供的功能非常多。每个服务网格实现所提供的功能也各有不同。下面我将对服务网格中的重要功能进行介绍。 自动代理注入为了使用服务网格提供的功能,应用服务的 Pod 需要添加服务代理容器,服务网格提供了自动的代理注入机制。在 Kubernetes 上,如果 Pod 或控制器对象中添加了某个特定的注解,则服务网格可以自动在 Pod 中添加服务代理容器并完成相关的配置。 流量管理 流量管理指的是管理服务之间的相互调用,由一系列的子功能组成。 服务发现 服务发现指的是发现系统中存在的服务及其对应的访问地址,服务网格会在内部维护一个注册表,包含所有发现的服务及其对应的服务端点。 负载均衡 每个服务通常都有多个运行的实例,在进行调用时,需要根据某些策略选择处理请求的实例。负载均衡的算法可以很简单,比如循环制(round robin);也可以很复杂,比如根据被调用服务的各个实例的负载情况来动态选择。 流量控制 微服务架构的应用强调持续集成和持续部署,应用的每个服务都可以被单独部署。一个常见的需求是在进行更新时,让小部分用户使用新的版本,而大部分用户仍然使用当前的旧版本,这样的更新方式称为金丝雀部署(Canary Deployment)。为了支持这样的更新方式,我们可以同时部署服务的两个版本,并通过服务网格把调用请求分配到两个版本,比如,20% 的请求分配到新版本,剩下 80% 的请求分配到当前版本,经过一段时间的测试之后,再逐步把更多的请求分配到新版本,直到全部请求分配至新版本。 超时处理 服务网格对服务调用添加了超时处理机制。如果调用在设置的时间之后仍然没有返回,则会直接出错,这样就避免了在被调用的服务出现问题时,进行不必要的等待。不过,超时时间也不能设置得过短,否则会有大量相对耗时的调用产生不必要的错误,针对这一点,服务网格提供了基于配置的方式来调整服务的超时时间。 重试 当服务的调用出现错误时,服务网格可以选择进行重试,服务重试看似简单,但要正确的实现并不容易。简单的重试策略,比如固定时间间隔和最大重试次数的做法,很容易产生重试风暴(Retry Storm)。如果某些请求因为服务负载的原因而失败,简单的重试策略会在固定的时间间隔之后,重试全部失败请求,这些请求在重试时又会因为负载过大的原因而再次失败。所造成的结果就是产生大量失败的重试请求,影响整体的性能,有效的重试机制应该避免出现重试风暴。 断路器 断路器(Circuit Breaker)是微服务架构中的一种常见模式。通过断路器,可以在服务的每个实例上设置限制,比如同时允许的最大连接数量,或是调用失败的次数。当设定的限制达到时,断路器会自动断开,禁止对该实例的连接。 断路器的存在,使得服务调用可以快速失败,而不用尝试连接一个已经失败或过载的实例,所以它的一个重要作用是避免服务的级联失败。如果一个服务出现错误,可能导致它的调用者因为超时而积压很多未处理的请求,进而导致它的调用者也由于负载过大而崩溃,这样的级联效应,有可能导致整个应用的崩溃。使用断路器之后,出现错误的服务实例被自动隔离,不会影响系统中的其他服务。 错误注入 在使用服务网格配置了服务的错误处理策略之后,一个重要的需求是对这些策略进行测试。错误注入指的是往系统中引入错误来测试应用的故障恢复能力,比如,错误注入可以在服务调用时自动添加延迟,或是直接返回错误给调用者。 安全安全相关的功能解决应用的 3 个 A 需求,分别是认证(Authentication)、授权(Authorization)和审计(Audit)。这3个需求的英文名称都以字母A开头,所以称为3个A需求。 双向 TLS(mutual TLS,mTLS)指的是在服务调用者和被调用者的服务代理之间建立双向 TLS 连接,这个连接意味着客户端和服务器都需要认证对方的身份。通过 TLS 连接可以对通信进行加密,防止中间人攻击。 用户认证:服务网格应该可以和不同的用户认证服务进行集成,常用的认证方式包括 JWT 令牌认证,以及与 OpenID Connect 提供者进行集成。 访问策略访问策略用来描述服务调用时的策略。 访问速率控制:通过访问速率控制,可以限制服务的调用速度,防止服务因请求过多而崩溃。 服务访问控制:服务访问控制用来限制对服务的访问,限制的方式包括禁止服务、黑名单和白名单等。 可观察性服务网格可以收集与服务之间通信相关的遥测数据,这些数据使得运维人员可以观察服务的行为,发现服务可能存在的问题,并对服务进行优化。 性能指标:是指服务网格收集与服务调用相关的性能指标数据,包括延迟、访问量、错误和饱和度。除此之外,服务网格还收集与自身的控制平面相关的数据。 分布式追踪:可以查看单个请求在服务网格中的处理流程,在微服务架构中,应用接收到的请求可能由多个服务协同处理。在请求延迟过高时,需要查看请求在不同服务之间的调用流程,以及每个服务所带来的延迟。分布式追踪是服务网格提供的工具,可以用来收集相关的调用信息。 访问日志:用来记录每个服务实例所接收到的请求。 服务网格产品介绍Istio 项目由 Google、IBM 和 Lyft 共同发起。由于有大公司的支持,Istio 项目目前所提供的功能是最完备的,这也意味着 Istio 是最复杂的。Istio 所包含的组件非常多,对应的配置也非常复杂,它的学习曲线很陡,上手并不容易。值得一提的是,Lyft 的 Envoy 团队与 Istio 有很好的合作,这就保证了 Istio 有最好的 Envoy 支持。本专栏将使用 Istio 来作为服务网格的实现。 Linkerd 是最早的服务网格实现,目前作为 CNCF 的项目来开发。相对 Istio 而言,Linkerd 提供的功能较少,但是也更简单易用。对很多应用来说,Linkerd 所提供的功能已经足够好。 Maesh 是 Containous 提供的服务网格实现。Maesh 使用 Traefik 作为服务代理。相对于 Istio 和 Linkerd,Maesh 还是一个比较新的项目,需要更多的时间来考察。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"后端","slug":"后端","permalink":"http://damon008.github.io/tags/%E5%90%8E%E7%AB%AF/"}]},{"title":"云产品年中大优惠","date":"2021-03-19T10:05:26.000Z","path":"2021/03/19/popularize-02/","text":"双十一阿里云全网最低:立即领取 阿里云产品大促:立即领取 专属折扣码: ============================================================================================================================= 【腾讯云】11.11 云上盛惠,云产品限时抢购,1核2G云服务器首年88元!立即秒杀 【腾讯云】境外1核2G服务器低至2折,半价续费券限量免费领取!立即秒杀 【腾讯云】新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得!立即秒杀 【腾讯云】热卖云产品3折起,云服务器、云数据库特惠,服务更稳,速度更快,价格更低!立即秒杀 【腾讯云】腾讯云数据库性能卓越稳定可靠,为您解决数据库运维难题立即秒杀 【腾讯云】云数据库MySQL基础版1元体验立即秒杀 腾讯云新客专属福利:立即秒杀 腾讯云十周年大促:立即秒杀 腾讯云服务器全球购:立即抢购 腾讯企业上云特惠活动:立即抢购 腾讯云产品三折:立即抢购 腾讯服务器X实时音视频 联合大促:立即抢购 腾讯云服务器购买页:立即抢购 腾讯云数据库购买页:立即抢购 腾讯云新客专属福利:立即抢购 腾讯云限时秒杀活动:立即抢购 腾讯云数据库Redis:立即抢购 ============================================================================================================================= 华为云新客专属福利:立即注册 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"服务器","slug":"服务器","permalink":"http://damon008.github.io/tags/%E6%9C%8D%E5%8A%A1%E5%99%A8/"}]},{"title":"备份Kubernetes的5个最佳实践","date":"2021-02-22T09:14:29.000Z","path":"2021/02/22/k8s-backup/","text":"备份应用程序和数据是组织经常需要处理的事情。尽管Kubernetes可以确保应用程序服务的高可用性和可伸缩性,但这些好处并不能有效地保护数据。因此,必须对Kubernetes应用程序进行数据管理和备份,并应将其纳入标准操作流程中。 但是,备份Kubernetes应用程序需要一种独特的方法,该方法与传统的备份解决方案大不相同。使用Kubernetes,经常会将应用程序部署在集群中跨节点的多个容器中,要备份应用程序以及数据和存储量,你需要考虑所有各种Kubernetes对象和配置数据,还必须适应应用程序快速的开发和部署周期,DevOps的“左移(shift-left)”理念,数据保护,安全要求等。 鉴于这些独特的要求,备份Kubernetes似乎是一项艰巨的任务,但是你可以采取一些步骤来简化该过程。以下是五个最佳做法: 1.考虑Kubernetes架构 一个典型的Kubernetes应用程序由数百个组件组成-Pod,服务(service),证书,密钥(secret)等等。任何Kubernetes备份解决方案不仅要能够备份和还原数据,而且还要能够备份和还原所有这些组件。至关重要的是,备份解决方案要通过API自动与Kubernetes控制平面进行交互,以便不仅能够发现集群上运行的Kubernetes应用,而且还可以与基础计算,网络和存储基础架构集成。 存储也是一个重要的考虑因素,必须包含在备份计划中。与应用程序配置数据一样,Kubernetes存储(用于应用程序容器的持久卷)包含需要保护的重要业务数据。 最后,确定要备份存储的位置。你将其保留在本地s存储还是在云中?灵活性和易用性将成为任何数据备份存储的重要特征。 2.制定恢复计划 由于Kubernetes应用程序的分布式架构,还原数据需要很多步骤。例如,你需要验证集群依赖关系,创建新的Kubernetes视图的替代数据,并确定在何处启动恢复。然后,你需要标识备份数据源并准备目标存储。一旦计划了这些,就必须更新所有组件以创建新的存储资源。提前创建详细计划可以帮助你引导这个复杂的过程,幸运的是,有些Kubernetes备份解决方案可以自动为你执行此操作,你应该寻找一种支持此功能的解决方案。 但是可靠的执行计划仅仅是开始。你还应该确保你的备份平台可以将各个步骤转换为相关的Kubernetes API调用。这样可确保恢复功能所需的资源可用,并确保正确部署和配置了云原生应用程序的所有组件。 3.简化操作 如果备份需要编码,打包或部署,则开发人员可能会避免使用它们。他们的目标是快速开发和部署应用程序,而复杂的备份过程可能会阻碍其进展。 因此,备份应由API驱动,并且是无缝衔接的。确保你的解决方案具有针对应用程序而不是其单个组件的自动备份策略,并具有在部署新应用程序时检测和备份新应用程序的能力。最后,确保你的备份解决方案提供了简单的工作流程,并使你的运维团队能够顺畅地遵守任何法规和监控要求。 4.确保安全 与任何数据管理功能一样,安全性至关重要。执行Kubernetes备份时,要实施身份和访问管理以及基于角色的访问管理(RBAC)的控件,以确保只有授权的用户和组才能访问备份平台。这使你可以控制谁可以执行任务,例如监视和验证备份,执行还原等,并使你可以向开发人员授予从快照还原应用程序的权限。 你的解决方案应集成到云提供商的身份验证解决方案中,而无需任何其他工具或API。最后,请确保你的数据已加密-无论是在传输中还是在静止状态。 5.利用Kubernetes的可移植性 要利用Kubernetes的可移植性功能,你的备份解决方案应该能够兼容几种发行版和基础架构配置执行还原,并自动转换应用程序的备份版本以在新环境中运行。 备份解决方案要能够转换所有应用程序依赖项以与新环境兼容,这一点很重要。 Kubernetes原生备份是你的最佳选择 无论你的目标是保护Kubernetes应用程序免受数据丢失和损坏,为测试和开发目的备份数据,将应用程序迁移到新环境中,还是支持组织的灾难恢复计划,备份对于高效运维都是必不可少的。 使用传统解决方案而不是专门为Kubernetes环境设计的解决方案会增加意外数据丢失和配置错误的风险,并且无法提供保护应用程序数据所需的细粒度,可感知的应用程序备份和恢复功能。为了遵守Kubernetes环境中的备份和恢复最佳实践,Kubernetes原生备份解决方案是最佳方法。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注 特别声明 原文作者:王延飞 本文原链:http://mtw.so/5GiprA 本文转载如有侵权,请联系站长删除,谢谢","tags":[{"name":"K8s","slug":"K8s","permalink":"http://damon008.github.io/tags/K8s/"}]},{"title":"Spring Cloud 与 K8s 在微服务层面的不同","date":"2021-02-04T02:18:16.000Z","path":"2021/02/04/spring-cloud-k8s/","text":"Spring Boot 1.x 与 2.x 的区别在《微服务 Spring Cloud 架构设计》一文中,笔者讲过 Spring Cloud 的架构设计。其实 Spring Boot 在一开始时,运用到的基本就是 Eureka、Config、Zuul、Ribbon、Feign、Hystrix 等。到了 Spring Boot 2.x 的时候,大量的组件开始风云崛起。下面简单列下这两个版本之间的区别如下。 Spring Boot 1.x 中,session 的超时时间是这样的: 1server.session.timeout=3600 而在 2.x 中: 1server.servlet.session.timeout=PT120M 截然不同的写法,cookie 也是一样的: 123456server: servlet: session: timeout: PT120M cookie: name: ORDER-SERVICE-SESSIONID 应用的 ContextPath 配置属性改动,跟上面的 session 一样,加上了一个 servlet。 Spring Boot 2.x 基于 Spring 5,而 Spring Boot 1.x 基于 Spring 4 或较低。 统一错误处理的基类 AbstarctErrorController 的改动。 配置文件的中文可以直接读取,不需要转码。 Acutator 变化很大,默认情况不再启用所有监控,需要定制化编写监控信息,完全需要重写,HealthIndicator,EndPoint 同理。 从 Spring Boot 2.x 开始,可以与 K8s 结合来实现服务的配置管理、负载均衡等,这是与 1.x 所不同的。 K8s 的一些资源的介绍上面说到 Spring Boot 2.x 可以结合 K8s 来作为微服务的架构设计,那么就先来说下 K8s 的一些组件吧。 ConfigMap,看到这个名字可以理解:它是用于保存配置信息的键值对,可以用来保存单个属性,也可以保存配置文件。对于一些非敏感的信息,比如应用的配置信息,则可以使用 ConfigMap。 创建一个 ConfigMap 有多种方式如下。 1. key-value 字符串创建 1kubectl create configmap test-config --from-literal=baseDir=/usr 上面的命令创建了一个名为 test-config,拥有一条 key 为 baseDir,value 为 “/usr” 的键值对数据。 2. 根据 yml 描述文件创建 123456apiVersion: v1kind: ConfigMapmetadata: name: test-configdata: baseDir: /usr 也可以这样,创建一个 yml 文件,选择不同的环境配置不同的信息: 123456789101112131415161718192021kind: ConfigMapapiVersion: v1metadata: name: cas-serverdata: application.yaml: |- greeting: message: Say Hello to the World --- spring: profiles: dev greeting: message: Say Hello to the Dev spring: profiles: test greeting: message: Say Hello to the Test spring: profiles: prod greeting: message: Say Hello to the Prod 注意点: ConfigMap 必须在 Pod 使用其之前创建。 Pod 只能使用同一个命名空间的 ConfigMap。 当然,还有其他更多用途,具体可以参考官网。 Service,顾名思义是一个服务,什么样的服务呢?它是定义了一个服务的多种 pod 的逻辑合集以及一种访问 pod 的策略。 service 的类型有四种: ExternalName:创建一个 DNS 别名指向 service name,这样可以防止 service name 发生变化,但需要配合 DNS 插件使用。 ClusterIP:默认的类型,用于为集群内 Pod 访问时,提供的固定访问地址,默认是自动分配地址,可使用 ClusterIP 关键字指定固定 IP。 NodePort:基于 ClusterIp,用于为集群外部访问 Service 后面 Pod 提供访问接入端口。 LoadBalancer:它是基于 NodePort。 如何使用 K8s 来实现服务注册与发现从上面讲的 Service,我们可以看到一种场景:所有的微服务在一个局域网内,或者说在一个 K8s 集群下,那么可以通过 Service 用于集群内 Pod 的访问,这就是 Service 默认的一种类型 ClusterIP,ClusterIP 这种的默认会自动分配地址。 那么问题来了,既然可以通过上面的 ClusterIp 来实现集群内部的服务访问,那么如何注册服务呢?其实 K8s 并没有引入任何的注册中心,使用的就是 K8s 的 kube-dns 组件。然后 K8s 将 Service 的名称当做域名注册到 kube-dns 中,通过 Service 的名称就可以访问其提供的服务。那么问题又来了,如果一个服务的 pod 对应有多个,那么如何实现 LB?其实,最终通过 kube-proxy,实现负载均衡。 说到这,我们来看下 Service 的服务发现与负载均衡的策略,Service 负载分发策略有两种: RoundRobin:轮询模式,即轮询将请求转发到后端的各个 pod 上,其为默认模式。 SessionAffinity:基于客户端 IP 地址进行会话保持的模式,类似 IP Hash 的方式,来实现服务的负载均衡。 其实,K8s 利用其 Service 实现服务的发现,其实说白了,就是通过域名进行层层解析,最后解析到容器内部的 ip 和 port 来找到对应的服务,以完成请求。 下面写一个很简单的例子: 123456789101112apiVersion: v1kind: Servicemetadata: name: cas-server-service namespace: defaultspec: ports: - name: cas-server01 port: 2000 targetPort: cas-server01 selector: app: cas-server 可以看到执行 kubectl apply -f service.yaml 后: 12345root@ubuntu:~$ kubectl get svcNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEadmin-web-service ClusterIP 10.16.129.24 <none> 2001/TCP 84dcas-server-service ClusterIP 10.16.230.167 <none> 2000/TCP 67dcloud-admin-service-service ClusterIP 10.16.25.178 <none> 1001/TCP 190d 这样,我们可以看到默认的类型是 ClusterIP,用于为集群内 Pod 访问时,可以先通过域名来解析到多个服务地址信息,然后再通过 LB 策略来选择其中一个作为请求的对象。 K8s 如何来处理微服务中常用的配置在上面,我们讲过了几种创建 ConfigMap 的方式,其中有一种在 Java 中常常用到:通过创建 yml 文件来实现配置管理。 比如: 123456789101112131415161718192021kind: ConfigMapapiVersion: v1metadata: name: cas-serverdata: application.yaml: |- greeting: message: Say Hello to the World --- spring: profiles: dev greeting: message: Say Hello to the Dev spring: profiles: test greeting: message: Say Hello to the Test spring: profiles: prod greeting: message: Say Hello to the Prod 上面创建了一个 yml 文件,同时,通过 spring.profiles 指定了开发、测试、生产等每种环境的配置。 具体代码: 1234567891011121314151617181920212223242526272829303132333435363738394041apiVersion: apps/v1kind: Deploymentmetadata: name: cas-server-deployment labels: app: cas-serverspec: replicas: 1 selector: matchLabels: app: cas-server template: metadata: labels: app: cas-server spec: nodeSelector: cas-server: \"true\" containers: - name: cas-server image: {{ cluster_cfg['cluster']['docker-registry']['prefix'] }}cas-server imagePullPolicy: Always ports: - name: cas-server01 containerPort: 2000 volumeMounts: - mountPath: /home/cas-server name: cas-server-path args: [\"sh\", \"-c\", \"nohup java $JAVA_OPTS -jar -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m -Xms1024m -Xmx1024m -Xmn256m -Xss256k -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC cas-server.jar --spring.profiles.active=dev\", \"&\"] hostAliases: - ip: \"127.0.0.1\" hostnames: - \"gemantic.localhost\" - ip: \"0.0.0.0\" hostnames: - \"gemantic.all\" volumes: - name: cas-server-path hostPath: path: /var/pai/cas-server 这样,当我们启动容器时,通过 --spring.profiles.active=dev 来指定当前容器的活跃环境,即可获取 ConfigMap 中对应的配置。是不是感觉跟 Java 中的 Config 配置多个环境的配置有点类似呢?但是,我们不用那么复杂,这些统统可以交给 K8s 来处理。只需要你启动这一命令即可,是不是很简单? Spring Boot 2.x 的新特性在第一节中,我们就讲到 1.x 与 2.x 的区别,其中最为凸显的是,Spring Boot 2.x 结合了 K8s 来实现微服务的架构设计。其实,在 K8s 中,更新 ConfigMap 后,pod 是不会自动刷新 configMap 中的变更,如果想要获取 ConfigMap 中最新的信息,需要重启 pod。 但 2.x 提供了自动刷新的功能: 123456789101112131415spring: application: name: cas-server cloud: kubernetes: config: sources: - name: ${spring.application.name} namespace: default discovery: all-namespaces: true reload: enabled: true mode: polling period: 500 如上,我们打开了自动更新配置的开关,并且设置了自动更新的方式为主动拉取,时间间隔为 500ms,同时,还提供了另外一种方式——event 事件通知模式。这样,在 ConfigMap 发生改变时,无需重启 pod 即可获取最新的数据信息。 同时,Spring Boot 2.x 结合了 K8s 来实现微服务的服务注册与发现: 123456789<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-kubernetes-core</artifactId></dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-kubernetes-discovery</artifactId></dependency> 开启服务发现功能: 12345spring: cloud: kubernetes: discovery: all-namespaces: true 开启后,我们在《微服务 Spring Cloud 架构设计》一文中讲过,其实最终是向 K8s 的 API Server 发起 http 请求,获取 Service 资源的数据列表。然后根据底层的负载均衡策略来实现服务的发现,最终解析到某个 pod 上。那么为了同一服务的多个 pod 存在,我们需要执行: 1kubectl scale --replicas=2 deployment admin-web-deployment 同时,我们如果通过 HTTP 的 RestTemplate Client 来作服务请求时,可以配置一些请求的策略,RestTemplate 一般与 Ribbon 结合使用: 12345678910111213141516171819202122232425client: http: request: connectTimeout: 8000 readTimeout: 3000backend: ribbon: eureka: enabled: false client: enabled: true ServerListRefreshInterval: 5000ribbon: ConnectTimeout: 8000 ReadTimeout: 3000 eager-load: enabled: true clients: cas-server-service,admin-web-service MaxAutoRetries: 1 #对第一次请求的服务的重试次数 MaxAutoRetriesNextServer: 1 #要重试的下一个服务的最大数量(不包括第一个服务) #ServerListRefreshInterval: 2000 OkToRetryOnAllOperations: true NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #com.damon.config.RibbonConfiguration #分布式负载均衡策略 可以配置一些服务列表,自定义一些负载均衡的策略。 如果你是使用 Feign 来作为 LB,其实与 Ribbon 只有一点点不一样,因为 Feign 本身是基于 Ribbon 来实现的,除了加上注解 @EnableFeignClients 后,还要配置: 1234567feign: client: config: default: #provider-service connectTimeout: 8000 #客户端连接超时时间 readTimeout: 3000 #客户端读超时设置 loggerLevel: full 其他的可以自定义负载均衡策略,这一点是基于 Ribbon 的,所以是一样的。 实战 Spring Boot 2.x 结合 K8s 来实现微服务架构设计微服务架构中,主要的就是服务消费者、服务的生产者可以互通,可以发生调用,在这基础上,还可以实现负载均衡,即一个服务调用另一个服务时,在该服务存在多个节点的情况下,可以通过一些策略来找到该服务的一个合适的节点访问。下面主要介绍服务的生产者与消费者。 先看生产者,引入常用的依赖: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.13.RELEASE</version> <relativePath/> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <swagger.version>2.6.1</swagger.version> <xstream.version>1.4.7</xstream.version> <pageHelper.version>4.1.6</pageHelper.version> <fastjson.version>1.2.51</fastjson.version> <springcloud.version>Greenwich.SR3</springcloud.version> <springcloud.kubernetes.version>1.1.1.RELEASE</springcloud.kubernetes.version> <mysql.version>5.1.46</mysql.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${springcloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId> </dependency> <!-- 配置加载依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-actuator-autoconfigure</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-kubernetes-config</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>4.6.3</version> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>19.0</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>${swagger.version}</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>${swagger.version}</version> </dependency> <!-- 数据库分页依赖 --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>${pageHelper.version}</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.1.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <!-- 数据库驱动 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.3</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>${fastjson.version}</version> </dependency> <dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.11.3</version> </dependency> </dependencies> 上面我们使用了比较新的版本:Spring Boot 2.1.13,Cloud 版本是 Greenwich.SR3,其次,我们配置了 K8s 的 ConfigMap 所用的依赖,加上了数据库的一些配置,具体其他的,实现过程中,大家可以自行添加。 接下来,我们看启动时加载的配置文件,这里加了关于 K8s ConfigMap 所管理的配置所在的信息,以及保证服务被发现,开启了所有的 namespace,同时还启动了配置自动刷新的功能,注意的是,该配置需要在 bootstrap 文件: 123456789101112131415161718spring: application: name: cas-server cloud: kubernetes: config: sources: - name: ${spring.application.name} namespace: default discovery: all-namespaces: true #发现所有的命令空间的服务 reload: enabled: true mode: polling #自动刷新模式为拉取模式,也可以是事件模式 event period: 500 #拉取模式下的频率logging: #日志路径设置 path: /data/${spring.application.name}/logs 剩下的一些配置可以在 application 文件中配置: 1234567891011121314151617181920212223spring: profiles: active: devserver: port: 2000 undertow: accesslog: enabled: false pattern: combined servlet: session: timeout: PT120M #session 超时时间client: http: request: connectTimeout: 8000 readTimeout: 30000mybatis: #持久层配置 mapperLocations: classpath:mapper/*.xml typeAliasesPackage: com.damon.*.model 接下来看下启动类: 12345678910111213141516/** * * @author Damon * @date 2020 年 1 月 13 日 下午 8:29:42 * */@Configuration@EnableAutoConfiguration@ComponentScan(basePackages = {\"com.damon\"})//@SpringBootApplication(scanBasePackages = { \"com.damon\" })@EnableConfigurationProperties(EnvConfig.class)public class CasApp { public static void main(String[] args) { SpringApplication.run(CasApp.class, args); }} 这里我们没有直接用注解 @SpringBootApplication,因为主要用到的就是几个配置,没必要全部加载。 我们看到启动类中有一个引入的 EnvConfig.class: 12345678910111213141516171819/** * @author Damon * @date 2019 年 10 月 25 日 下午 8:54:01 * */@Configuration@ConfigurationProperties(prefix = \"greeting\")public class EnvConfig { private String message = \"This is a dummy message\"; public String getMessage() { return this.message; } public void setMessage(String message) { this.message = message; } 这就是配置 ConfigMap 中的属性的类。剩下的可以自己定义一个接口类,来实现服务生产者。 最后,我们需要在 K8s 下部署的话,需要准备几个脚本。 1. 创建 ConfigMap 123456789101112131415161718192021kind: ConfigMapapiVersion: v1metadata: name: cas-serverdata: application.yaml: |- greeting: message: Say Hello to the World --- spring: profiles: dev greeting: message: Say Hello to the Dev spring: profiles: test greeting: message: Say Hello to the Test spring: profiles: prod greeting: message: Say Hello to the Prod 设置了不同环境的配置,注意,这里的 namespace 需要与服务部署的 namespace 一致,这里默认的是 default,而且在创建服务之前,先得创建这个。 2. 创建服务部署脚本 12345678910111213141516171819202122232425262728293031323334353637383940414243apiVersion: apps/v1kind: Deploymentmetadata: name: cas-server-deployment labels: app: cas-serverspec: replicas: 3 selector: matchLabels: app: cas-server template: metadata: labels: app: cas-server spec: nodeSelector: cas-server: \"true\" containers: - name: cas-server image: cas-server imagePullPolicy: Always ports: - name: cas-server01 containerPort: 2000 volumeMounts: - mountPath: /home/cas-server name: cas-server-path - mountPath: /data/cas-server name: cas-server-log-path - mountPath: /etc/kubernetes name: kube-config-path args: [\"sh\", \"-c\", \"nohup java $JAVA_OPTS -jar -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m -Xms1024m -Xmx1024m -Xmn256m -Xss256k -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC cas-server.jar --spring.profiles.active=dev\", \"&\"] volumes: - name: cas-server-path hostPath: path: /var/pai/cas-server - name: cas-server-log-path hostPath: path: /data/cas-server - name: kube-config-path hostPath: path: /etc/kubernetes 注意:这里有个属性 replicas,其作用是当前 pod 所启动的副本数,即我们常说的启动的节点个数,当然,你也可以通过前面讲的脚本来执行生成多个 pod 副本。如果这里没有设置多个的话,也可以通过命令来执行: 1kubectl scale --replicas=3 deployment cas-server-deployment 这里,我建议使用 Deployment 类型的来创建 pod,因为 Deployment 类型更好的支持弹性伸缩与滚动更新。 同时,我们通过 --spring.profiles.active=dev 来指定当前 pod 的运行环境。 3. 创建一个 Service 最后,如果服务想被发现,需要创建一个 Service: 123456789101112apiVersion: v1kind: Servicemetadata: name: cas-server-service namespace: defaultspec: ports: - name: cas-server01 port: 2000 targetPort: cas-server01 selector: app: cas-server 注意,这里的 namespace 需要与服务部署的 namespace 一致,这里默认的是 default。 看看服务的消费者,同样,先看引入常用的依赖: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.13.RELEASE</version> <relativePath/> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <swagger.version>2.6.1</swagger.version> <xstream.version>1.4.7</xstream.version> <pageHelper.version>4.1.6</pageHelper.version> <fastjson.version>1.2.51</fastjson.version> <springcloud.version>Greenwich.SR3</springcloud.version> <!-- <springcloud.version>2.1.8.RELEASE</springcloud.version> --> <springcloud.kubernetes.version>1.1.1.RELEASE</springcloud.kubernetes.version> <mysql.version>5.1.46</mysql.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${springcloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- 配置加载依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-actuator-autoconfigure</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-kubernetes-config</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-commons</artifactId> </dependency> <!-- 结合 k8s 实现服务发现 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-kubernetes-core</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-kubernetes-discovery</artifactId> </dependency> <!-- 负载均衡策略 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-kubernetes-ribbon</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </dependency> <!-- 熔断机制 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>4.6.3</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>${fastjson.version}</version> </dependency> <dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.11.3</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>${swagger.version}</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>${swagger.version}</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.2</version> </dependency> <!-- 数据库分页 --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>${pageHelper.version}</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.1.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <!-- 数据库驱动 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.3</version> </dependency> </dependencies> 这里大部分的依赖跟生产者一样,但,需要加入服务发现的依赖,以及所用的负载均衡的策略依赖、服务的熔断机制。 接下来 bootstrap 文件中的配置跟生产者一样,这里不在说了,唯一不同的是 application 文件: 1234567891011121314151617181920212223242526272829backend: ribbon: eureka: enabled: false client: enabled: true ServerListRefreshInterval: 5000ribbon: ConnectTimeout: 3000 ReadTimeout: 1000 eager-load: enabled: true clients: cas-server-service,edge-cas-service,admin-web-service #负载均衡发现的服务列表 MaxAutoRetries: 1 #对第一次请求的服务的重试次数 MaxAutoRetriesNextServer: 1 #要重试的下一个服务的最大数量(不包括第一个服务) OkToRetryOnAllOperations: true NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #负载均衡策略hystrix: command: BackendCall: execution: isolation: thread: timeoutInMilliseconds: 5000 #熔断机制设置的超时时间 threadpool: BackendCallThread: coreSize: 5 引入了负载均衡的机制以及策略(可以自定义策略)。 接下来看启动类: 123456789101112131415161718/** * @author Damon * @date 2020 年 1 月 13 日 下午 9:23:06 * */@Configuration@EnableAutoConfiguration@ComponentScan(basePackages = {\"com.damon\"})@EnableConfigurationProperties(EnvConfig.class)@EnableDiscoveryClientpublic class AdminApp { public static void main(String[] args) { SpringApplication.run(AdminApp.class, args); }} 同样的 EnvConfig 类,这里不再展示了。其他的比如:注解 @EnableDiscoveryClient 是为了服务发现。 同样,我们新建接口,假如我们生产者有一个接口是: 1http://cas-server-service/api/getUser 则,我们在调用它时,可以通过 RestTemplate Client 来直接调用,通过 Ribbon 来实现负载均衡: 123456789@LoadBalanced @Bean public RestTemplate restTemplate() { SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); requestFactory.setReadTimeout(env.getProperty(\"client.http.request.readTimeout\", Integer.class, 15000)); requestFactory.setConnectTimeout(env.getProperty(\"client.http.request.connectTimeout\", Integer.class, 3000)); RestTemplate rt = new RestTemplate(requestFactory); return rt; } 可以看到,这种方式的分布式负载均衡实现起来很简单,直接注入一个初始化 Bean,加上一个注解 @LoadBalanced 即可。 在实现类中,我们只要直接调用服务生产者: 1ResponseEntity<String> forEntity = restTemplate.getForEntity(\"http://cas-server/api/getUser\", String.class); 其中,URL 中 必须要加上 \"http://\",这样即可实现服务的发现以及负载均衡,其中,LB 的策略,可以采用 Ribbon 的几种方式,也可以自定义一种。 最后,可以在实现类上加一个熔断机制: 12345678910@HystrixCommand(fallbackMethod = \"admin_service_fallBack\")public Response<Object> getUserInfo(HttpServletRequest req, HttpServletResponse res) { ResponseEntity<String> forEntity = restTemplate.getForEntity(envConfig.getCas_server_url() + \"/api/getUser\", String.class); logger.info(\"test restTemplate.getForEntity(): {}\", forEntity); if (forEntity.getStatusCodeValue() == 200) { logger.info(\"================================test restTemplate.getForEntity(): {}\", JSON.toJSON(forEntity.getBody())); logger.info(JSON.toJSONString(forEntity.getBody())); }} 其中发生熔断时,回调方法: 12345private Response<Object> admin_service_fallBack(HttpServletRequest req, HttpServletResponse res) { String token = StrUtil.subAfter(req.getHeader(\"Authorization\"), \"bearer \", false); logger.info(\"admin_service_fallBack token: {}\", token); return Response.ok(200, -5, \"服务挂啦!\", null); } 其返回的对象必须与原函数一致,否则可能会报错。具体的可以参考《Spring cloud 之熔断机制》。 最后与生产者一样,需要创建 ConfigMap、Service、服务部署脚本,下面会开源这些代码,这里也就不一一展示了。最后,我们会发现:当请求 认证中心时,认证中心存在的多个 pod,可以被轮训的请求到。这就是基于 Ribbon 的轮训策略来实现分布式的负载均衡,并且基于 Redis 来实现信息共享。 结束福利开源几个微服务的架构设计项目: https://github.com/damon008/spring-cloud-oauth2 https://github.com/damon008/spring-cloud-k8s https://gitee.com/damon_one/spring-cloud-k8s https://gitee.com/damon_one/spring-cloud-oauth2 欢迎大家 star,多多指教。 关于作者笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号程序猿 Damon 发起人。个人微信 DamonStatham,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"后端","slug":"后端","permalink":"http://damon008.github.io/tags/%E5%90%8E%E7%AB%AF/"},{"name":"Java","slug":"Java","permalink":"http://damon008.github.io/tags/Java/"},{"name":"K8s","slug":"K8s","permalink":"http://damon008.github.io/tags/K8s/"}]},{"title":"Volcano 作业资源预留设计原理解读","date":"2020-12-28T06:37:04.000Z","path":"2020/12/28/volcano-reserve/","text":"简介Volcano 是一个基于 Kubernetes 的云原生批量计算平台,也是 CNCF 的首个批量计算项目。Volcano 主要用于 AI、大数据、基因、渲染等诸多高性能计算场景,对主流通用计算框架均有很好的支持。它提供高性能计算任务调度,异构设备管理,任务运行时管理等能力。本篇文章将深度剖析 Volcano 重要特性之——资源预留。 场景分析在实际应用中,常见以下两种场景: 在集群资源不足的情况下,假设处于待调度状态的作业 A 和 B,A 资源申请量小于 B 或 A 优先级高于 B。基于默认调度策略,A 将优先于 B 进行调度。在最坏的情况下,若后续持续有高优先级或申请资源量较少的作业加入待调度队列,B 将长时间处于饥饿状态并永远等待下去。 在集群资源不足的情况下,假设存在待调度作业 A 和 B。A 优先级低于 B 但资源申请量小于 B。在基于集群吞吐量和资源利用率为核心的调度策略下,A 将优先被调度。在最坏的情况下,B 将持续饥饿下去。 以上两种场景出现的根因是缺少一种公平调度机制:保证长期处于饥饿状态的作业在达到某个临界条件后被优先调度。造成作业持久饥饿的原因很多,包括资源申请量长时间无法满足、优先级持续过低、抢占发生频率过高、亲和性无法满足(v1.1.0 暂不支持此场景)等,以资源申请量无法满足最为常见。 特性设计为了保证长期处于阻塞状态的作业能够拥有公平的调度机会,需要解决两个主要问题: 如何识别目标作业? 如何为目标作业预留资源? 目标作业识别作业条件作业条件的选定可以基于等待时间、资源申请量等单个维度或多个维度的组合。综合考虑,v1.1.0 实现版本选择优先级最高且等待时间最长的作业作为目标作业。这样不仅可以保证紧急任务优先被调度,等待时间长度的考虑默认筛选出了资源需求较多的作业。 作业数量客观来说,满足条件的作业通常不止一个,可以为目标作业组或单个目标作业预留资源。考虑到资源预留必然引起调度器性能在吞吐量和延时等方面的影响,v1.1.0 采用了单个目标作业的方式。 识别方式识别方式有两种:自定义配置和自动识别。v1.1.0 暂时仅支持自动识别方式,即调度器在每个调度周期自动识别符合条件和数量的目标作业,并为其预留资源。后续版本将考虑在全局和 Queue 粒度支持自定义配置。 资源预留算法资源预留算法是整个特性的核心。v1.1.0 采用节点组锁定的方式为目标作业预留资源,即选定一组符合某些约束条件的节点纳入节点组,节点组内的节点从纳入时刻起不再接受新作业投递,节点规格总和满足目标作业要求。需要强调的是,目标作业将可以在整个集群中进行调度,非目标作业仅可使用节点组外的节点进行调度。 节点选取在特性设计阶段,社区考虑过以下节点选取算法:规格优先、空闲优先。 规格优先是指集群中所有节点按照主要规格(目标作业申请资源规格)进行降序排序,选取前 N 个节点纳入节点组,这 N 个节点的资源总量满足申请量。这种方式的优点是实现简单、锁定节点数量最小化、对目标作业的调度友好(这种方式锁定的资源总量往往比申请总量大一些,且作业中各 Pod 容易聚集调度在锁定节点,有利于 Pod 间通信等);缺点是锁定资源总量大概率不是最优解、综合调度性能损失(吞吐量、调度时长)、易产生大资源碎片。v1.1.0 的实现采用的是该算法。 空闲优先是指集群中所有节点按照主要资源类型(目标作业申请资源类型)的空闲资源量进行降序排序,选取前 N 个节点纳入节点组,这 N 个节点的资源总量满足申请量。这种方式的优点是较大概率最快腾出满足要求的资源总量;缺点是集群空闲资源分布的强动态性导致节点组不是最优解,所求解稳定性差。 节点数量为了尽可能减少锁定操作对调度器综合性能的影响,在满足预留资源申请量的前提下,无论采用哪种节点选取算法,都应保证所选节点数最少。 锁定方式锁定方式包括两个核心考量点:并行锁定数量、锁定节点已有负载处理手段。 并行锁定数量有三个选择:单节点锁定、多节点锁定、集群锁定。单节点锁定是指每个调度周期内基于当前集群资源分布选定一个符合要求的节点纳入节点组。这种方式可以尽量减少资源分布波动对所求解的稳定性的影响,缺点是要经过较多的调度周期才能完成锁定过程。v1.1.0 的实现选择的是这种方式。 以此类推,多节点锁定是指每个调度周期内选定 X(X>1)个满足条件的节点进行锁定。这种方式能一定程度上弥补单节点锁定引入的锁定时长过长问题,缺点是 X 不易找到最优值,实现复杂度高。 集群锁定是指一次性锁定集群所有节点,直至目标作业完成调度。这种粗暴的方式实现最为简单,目标作业等待时间最短,非常适合超大目标作业的资源预留。 锁定节点已有负载的处理手段有两种:抢占式预留、非抢占式预留。顾名思义,抢占式预留将会强制驱逐锁定节点上的已有负载。这种方式可以保证最快腾出所需的资源申请量,但会对已有业务造成重大影响,因此仅适用于紧急任务的资源预留。非抢占式预留则在节点锁定后不做任何处理,等待运行在其上的负载自行结束。v1.1.0 采用的是非抢占式预留。 最佳实践基于 v1.1.0 的实现,社区当前仅支持目标作业的自动化识别与资源预留。为此,新引入了 2 个 action 和 1 个 plugin。elect action 用于选取目标作业;reserve action 用于执行资源预留动作;reservation plugin 中实现了具体的目标选取和资源预留逻辑。 若要开启资源预留特性,将以上 action 和 plugin 配置到 volcano 的配置文件中即可。 下面是推荐配置样例: 12345678910111213actions: \"enqueue, elect, allocate, backfill, reserve\"tiers:- plugins:- name: priority- name: gang- name: conformance- name: reservation- plugins:- name: drf- name: predicates- name: proportion- name: nodeorder- name: binpack 自行配置时,请注意以下事项: elect action 必须配置在 enqueue action 和 allocate action 之间 reserve action 必须配置在 allocate action 之后 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"volcano","slug":"volcano","permalink":"http://damon008.github.io/tags/volcano/"}]},{"title":"Go 并发基础","date":"2020-12-23T07:17:50.000Z","path":"2020/12/23/study-go/","text":"协程(Goroutine)我们知道 Go 中,存在一个 defer 关键字用于修饰一个函数或者方法,使得该函数或者方法在返回前才会执行,也就说被延迟执行,但又一定会执行。但其实 Go 中也存在类似的异步,或者说多线程的概念,但在 Go 中不叫作线程,而是叫协程。 协程相对于线程来说,是一个非常轻量级的东西,它在一个程序中,可以启动很多个。协程也称为 goroutine。goroutine 被 Go runtime 所调度,这一点和线程不一样。也就是说,Go 语言的并发是由 Go 自己所调度的,自己决定同时执行多少个 goroutine,什么时候执行哪几个。这些对于我们开发者来说很透明,只需要在编码的时候告诉 Go 语言要启动几个 goroutine,至于如何调度执行,我们不用关心。 启动一个 goroutine 简单,Go 语言为我们提供了 go 关键字,相比其他编程语言简化了很多,如代码: 123456789func main() { go fmt.Println(\"码疯窝在香嗝喱辣\") fmt.Println(\"I am main goroutine\") time.Sleep(time.Second)} 这样就启动了一个 goroutine,用来调用 fmt.Println 函数,打印”码疯窝在香嗝喱辣”,所以这段代码里,其实有两个 goroutine,一个是 main 函数启动的 main goroutine,一个是通过 go 关键字启动的 goroutine。 也就是说,启动一个协程的关键字 go 即可,语法: 123go function()go 函数执行体 go 关键字后跟一个方法或者函数的调用,就可以启动一个 goroutine,让方法在这个新启动的 goroutine 中运行。运行以上示例,可以看到如下输出: 12345I am main goroutine#待一秒的同时输出下面码疯窝在香嗝喱辣 从输出结果也可以看出,程序是并发的,go 关键字启动的 goroutine 并不阻塞 main goroutine 的执行,所以我们看到如上打印。 在 Go 中,既然有了协程,那么这些协程之间如何通信呢?Go 提供了一个 channel(通道) 来解决。 声明一个 channel在 Go 语言中,声明一个 channel 非常简单,使用内置的 make 函数即可,如下: 1ch:=make(chan string) 其中 chan 是一个关键字,表示是 channel 类型。后面的 string 表示 channel 里的数据是 string 类型。通过 channel 的声明也可以看到,chan 是一个集合类型。 定义好 chan 后就可以使用了,一个 chan 的操作只有两种:发送和接收: 发送:向 chan 发送值,把值放在 chan 中,操作符为 chan <- 接收:获取 chan 中的值,操作符为 <- chan 示例: 1234567891011121314151617181920212223package mainimport \"fmt\"func main() { ch := make(chan string) go func() { fmt.Println(\"码疯窝在香嗝喱辣\") ch <- \"发送数据者:码疯窝在香嗝喱辣\" }() fmt.Println(\"I am main goroutine\") v := <- ch fmt.Println(\"接收到的chan中的值为:\",v)} 我们先来执行看看打印结果: 12345I am main goroutine码疯窝在香嗝喱辣接收到的chan中的值为:送数据者:码疯窝在香嗝喱辣 从运行结果可以看出:达到了使用 time.Sleep 函数的效果。 相信应该明白为什么程序不会在新的 goroutine 完成之前退出了,因为通过 make 创建的 chan 中没有值,而 main goroutine 又想从 chan 中获取值,获取不到就一直等待,等到另一个 goroutine 向 chan 发送值为止。 无缓冲 channel上面的示例中,使用 make 创建的 chan 就是一个无缓冲 channel,它的容量是 0,不能存储任何数据。所以无缓冲 channel 只起到传输数据的作用,数据并不会在 channel 中做任何停留。这也意味着,无缓冲 channel 的发送和接收操作是同时进行的,它也被称为同步 channel。 有缓冲 channel有缓冲 channel 类似一个可阻塞的队列,内部的元素先进先出。通过 make 函数的第二个参数可以指定 channel 容量的大小,进而创建一个有缓冲 channel,如: 1cacheCh := make(chan int,5) 定义了一个容量为 5 的元素为 int 类型的 chan。 一个有缓冲 channel 具备以下特点: 有缓冲 channel 的内部有一个缓冲队列 发送操作是向队列的尾部插入元素,如果队列已满,则阻塞等待,直到另一个 goroutine 执行,接收操作释放队列的空间 接收操作是从队列的头部获取元素并把它从队列中删除,如果队列为空,则阻塞等待,直到另一个 goroutine 执行,发送操作插入新的元素 1234567cache := make(chan int,5)cache <- 2cache <- 3fmt.Println(\"容量:\",cap(cache),\",元素个数:\",len(cache)) 无缓冲 channel 其实就是一个容量大小为 0 的 channel。比如 make(chan int,0) 关闭 channel通过内置函数 close 即可关闭 channel。如果一个 channel 被关闭了,就不能向里面发送数据了,如果发送的话,会引起 painc 异常。但是还可以接收 channel 里的数据,如果 channel 里没有数据的话,接收的数据是元素类型的零值。 单向 channel所谓单向,即可要不发送,要么只能接收。所以单向 channel 的声明也很简单,只需要在声明的时候带上 <- 操作符即可,如下: 12send := make(chan <- int)receive := make(<- chan int) 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"Go","slug":"Go","permalink":"http://damon008.github.io/tags/Go/"}]},{"title":"云原生时代跨语言间微服务的打法","date":"2020-12-22T10:28:34.000Z","path":"2020/12/22/microservice-k8s/","text":"1. K8s 组件 configMap kube-apiserver scheduler etcd controller kube-proxy K8s 中主要通过 kube-proxy 负责为 Service 提供 cluster 内部的服务发现和负载均衡,它是 K8s 集群内部的负载均衡器,也是一个分布式代理服务器,在 K8s 的每个节点上都有一个,这一设计体现了它的伸缩性优势,需要访问服务的节点越多,提供负载均衡能力的 kube-proxy 就越多,高可用节点也随之增多。通过 K8s service 的 “ClusterIP” 来实现集群内服务的 LB,当然,如果集群外需要访问 Service 对应的所有具有相同功能的 pod 应用程序,则可以通过 K8s service 的另外一种方式来实现:”NodePort”。 2. 基于 Service 实现微服务负载均衡在 Java 语言,或其它语言中,通常需要做很多繁重的组件来实现服务的 LB。例如:Dubbo、SpringCloud、甚至 SpringCloudAlibaba 等。当然,对于 Python、Go 等语言,也有其 Restful API,所以也会集成标准的代理插件来进行做传统的 LB。但对于云原生时代的到来,服务容器化让微服务的访问更好了。K8s Service 提供的 LB,即为无语言边际的负载均衡,不用考虑任何语言的阻碍,只要是通用的 Restful API,即可借助 service 来进行处理集群内部微服务之间的 LB。 在 K8s 集群中,如果内部访问,可以简单的通过 servicename 来进行访问。例如: 123456789101112apiVersion: v1kind: Servicemetadata: name: web-server-service namespace: defaultspec: ports: - name: web-server port: 80 targetPort: web-server-port selector: app: web-server 通过 selector 将 service 与服务 pod 对应起来,创建一个微服务的 service,默认其形式是:ClusterIP,则可以通过如下来访问该 service 对应的后端 pod: 1curl http://$service_name.$namespace.svc.cluster.local:$service_port/api/v1/*** 这里,K8s 通过虚拟出一个集群 IP,利用 kube-proxy 为 service 提供 cluster 内的服务发现和负载均衡,上面说了 kube-proxy 的功能。 123456789Name: web-server-serviceNamespace: defaultType: ClusterIPIP: 20.16.249.134Port: <unset> 80/TCPTargetPort: 80/TCPEndpoints: 20.162.35.223:80Session Affinity: NoneEvents: <none> 3. 高可用案例3.1 传统微服务请求案例传统的微服务中,不同语言构建的微服务架构很多,一般直接通过 http 协议进行访问,在同一种语言中,又会出现一种集成框架模式来实现微服务架构。如:Java 中 Dubbo、Springcloud 等,但其繁琐的框架结构导致了服务的繁重。 3.2 跨语言间微服务的互通在 k8s 集群内,通过 kube-proxy 结合 service 等一些功能组件来实现微服务之间的调用,不管是同语言也好,跨语言也罢。都会很好的进行处理,包括实现高可用以及负载均衡、服务治理等。 任何一个 k8s 集群中的 pod 都可以通过 http 协议来访问其它 pod 的服务: 12345678root@rest-server-ver2-ds-vcfc7:/usr/src/app# curl http://web-server-service.kube-system.svc.cluster.local:80/api/v1/healthz{ \"status\": { \"code\": 0, \"msg\": \"success\" }, \"data\": \"success\"}root@rest-server-ver2-ds-vcfc7:/usr/src/app# 其中的权限有的可以通过 namespace 来控制,有的可以通过服务本身的访问权限来控制,但一切都可以进行访问,不存在语言的差别对待。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"后端","slug":"后端","permalink":"http://damon008.github.io/tags/%E5%90%8E%E7%AB%AF/"}]},{"title":"极客时间专栏分享","date":"2020-12-16T07:12:38.000Z","path":"2020/12/16/geekbang-time/","text":"欢迎关注 福利 开源实战微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。","tags":[{"name":"极客","slug":"极客","permalink":"http://damon008.github.io/tags/%E6%9E%81%E5%AE%A2/"}]},{"title":"k8s 集群下微服务 pod 的各种指标信息监控","date":"2020-11-04T08:28:57.000Z","path":"2020/11/04/monitor-pod-state-metrics/","text":"今天主要分享下,在 k8s 集群下,微服务的各种状态指标情况的监控,我们都知道Prometheus是做数据监控的,但说白点,其独特格式的数据,其实都是靠一些源来的,那么这些源有哪些呢?已经有了cadvisor、heapster、metric-server,几乎容器运行的所有指标都能拿到,但是下面这种情况却无能为力: 1234调度了N个replicas?现在可用的有 N 个?N 个 Pod 是 running/stopped/terminated 状态?Pod 重启了N次?我有 N 个job在运行中 而这些则是 kube-state-metrics 提供的内容,它基于client-go开发,轮询Kubernetes API,并将Kubernetes的结构化信息转换为metrics。kube-state-metrics是kubernetes开源的一个插件。 废话不多说,直接上教程。。。 部署教程下载 在官网 kube-state-metrics 下载相应的源码以及部署脚本,本次使用release1.9.7,即v1.9.7版本的 kube-state-metrics 执行 cd /kube-state-metrics/examples/standard,可以看到几个文件: 123456cluster-role-binding.yamlcluster-role.yamldeployment.yamlprometheus-configmap.yamlservice-account.yamlservice.yaml 如果Prometheus已经部署,且部署在kube-system空间下,则源码中的namespace不需更改,否则可自定义为monitoring。 更新 首先修改 service.yaml 123456789101112131415161718192021apiVersion: v1kind: Servicemetadata: annotations: prometheus.io/scrape: \"true\" labels: app.kubernetes.io/name: kube-state-metrics app.kubernetes.io/version: v1.9.7 name: kube-state-metrics namespace: kube-systemspec: clusterIP: None ports: - name: http-metrics port: 8080 targetPort: http-metrics - name: telemetry port: 8081 targetPort: telemetry selector: app.kubernetes.io/name: kube-state-metrics 很简单,增加了注解方便后面使用 坑:源码中的角色授权绑定的是其写的kind为ClusterRole的资源,但后来发现部署kube-state-metrics服务时,其无法成功访问k8s的api-server,故需要修改,弃用其ClusterRole,使用k8s系统最高权限cluster-admin。 更改访问权限 vi cluster-role-binding.yaml 123456789101112131415apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: labels: app.kubernetes.io/name: kube-state-metrics app.kubernetes.io/version: v1.9.7 name: kube-state-metricsroleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-admin #kube-state-metricssubjects:- kind: ServiceAccount name: kube-state-metrics namespace: kube-system 部署12cd /kube-state-metrics/examples/standardkubectl create -f . 此时还需要更新Prometheus的挂载的configMap,因为前面说了只抓取带有prometheus.io/scrape: “true”注解的endpoint vi prometheus-configmap.yaml 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142apiVersion: v1kind: ConfigMapmetadata: name: prometheus-config namespace: kube-systemdata: prometheus.yaml: | global: scrape_interval: 15s evaluation_interval: 15s scrape_configs: - job_name: 'kubernetes-apiservers' kubernetes_sd_configs: - role: endpoints scheme: https tls_config: ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token relabel_configs: - source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name] action: keep regex: default;kubernetes;https - job_name: 'kubernetes-nodes' kubernetes_sd_configs: - role: node scheme: https tls_config: ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token relabel_configs: - action: labelmap regex: __meta_kubernetes_node_label_(.+) - target_label: __address__ replacement: kubernetes.default.svc:443 - source_labels: [__meta_kubernetes_node_name] regex: (.+) target_label: __metrics_path__ replacement: /api/v1/nodes/${1}/proxy/metrics - job_name: 'kubernetes-cadvisor' kubernetes_sd_configs: - role: node scheme: https tls_config: ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token relabel_configs: - action: labelmap regex: __meta_kubernetes_node_label_(.+) - target_label: __address__ replacement: kubernetes.default.svc:443 - source_labels: [__meta_kubernetes_node_name] regex: (.+) target_label: __metrics_path__ replacement: /api/v1/nodes/${1}/proxy/metrics/cadvisor - job_name: 'kubernetes-service-endpoints' kubernetes_sd_configs: - role: endpoints relabel_configs: - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape] action: keep regex: true - action: labelmap regex: __meta_kubernetes_service_label_(.+) - source_labels: [__meta_kubernetes_namespace] action: replace target_label: kubernetes_namespace - source_labels: [__meta_kubernetes_service_name] action: replace target_label: service_name - job_name: 'kubernetes-services' kubernetes_sd_configs: - role: service metrics_path: /probe params: module: [http_2xx] relabel_configs: - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_probe] action: keep regex: true - source_labels: [__address__] target_label: __param_target - target_label: __address__ replacement: blackbox-exporter.example.com:9115 - source_labels: [__param_target] target_label: instance - action: labelmap regex: __meta_kubernetes_service_label_(.+) - source_labels: [__meta_kubernetes_namespace] target_label: kubernetes_namespace - source_labels: [__meta_kubernetes_service_name] target_label: kubernetes_name - job_name: 'kubernetes-ingresses' kubernetes_sd_configs: - role: ingress relabel_configs: - source_labels: [__meta_kubernetes_ingress_annotation_prometheus_io_probe] action: keep regex: true - source_labels: [__meta_kubernetes_ingress_scheme,__address__,__meta_kubernetes_ingress_path] regex: (.+);(.+);(.+) replacement: ${1}://${2}${3} target_label: __param_target - target_label: __address__ replacement: blackbox-exporter.example.com:9115 - source_labels: [__param_target] target_label: instance - action: labelmap regex: __meta_kubernetes_ingress_label_(.+) - source_labels: [__meta_kubernetes_namespace] target_label: kubernetes_namespace - source_labels: [__meta_kubernetes_ingress_name] target_label: kubernetes_name - job_name: 'kubernetes-pods' kubernetes_sd_configs: - role: pod relabel_configs: - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape] action: keep regex: true - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path] action: replace target_label: __metrics_path__ regex: (.+) - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port] action: replace regex: ([^:]+)(?::\\d+)?;(\\d+) replacement: $1:$2 target_label: __address__ - action: labelmap regex: __meta_kubernetes_pod_label_(.+) - source_labels: [__meta_kubernetes_namespace] action: replace target_label: kubernetes_namespace - source_labels: [__meta_kubernetes_pod_name] action: replace target_label: kubernetes_pod_name 更新 configmap 后,需要重启 Prometheus 使其生效,如果没部署,则创建 configmap 后执行脚本部署即可。 导入模板 最后从 grafana.com 下载 state-metrics 监控模版导入模板Json 格式: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080{ \"__inputs\": [ { \"name\": \"DS_PROMETHEUS\", \"label\": \"Prometheus\", \"description\": \"\", \"type\": \"datasource\", \"pluginId\": \"prometheus\", \"pluginName\": \"Prometheus\" } ], \"__requires\": [ { \"type\": \"grafana\", \"id\": \"grafana\", \"name\": \"Grafana\", \"version\": \"4.6.1\" }, { \"type\": \"panel\", \"id\": \"graph\", \"name\": \"Graph\", \"version\": \"\" }, { \"type\": \"datasource\", \"id\": \"prometheus\", \"name\": \"Prometheus\", \"version\": \"1.0.0\" }, { \"type\": \"panel\", \"id\": \"singlestat\", \"name\": \"Singlestat\", \"version\": \"\" } ], \"annotations\": { \"list\": [ { \"builtIn\": 1, \"datasource\": \"-- Grafana --\", \"enable\": true, \"hide\": true, \"iconColor\": \"rgba(0, 211, 255, 1)\", \"name\": \"Annotations & Alerts\", \"type\": \"dashboard\" } ] }, \"description\": \"Monitor web applications through cAdvisor and Prometheus client_ruby.\", \"editable\": true, \"gnetId\": 3816, \"graphTooltip\": 0, \"hideControls\": false, \"id\": null, \"links\": [], \"refresh\": false, \"rows\": [ { \"collapse\": false, \"height\": 272, \"panels\": [ { \"aliasColors\": { \"80th percentile\": \"#bf1b00\", \"80th percentile \": \"#bf1b00\", \"90th percentile\": \"#508642\", \"90th percentile \": \"#eab839\", \"99th\": \"#58140c\", \"99th percentile\": \"#1f78c1\", \"99th percentile \": \"#1f78c1\" }, \"bars\": false, \"dashLength\": 10, \"dashes\": false, \"datasource\": \"${DS_PROMETHEUS}\", \"fill\": 5, \"id\": 46, \"legend\": { \"alignAsTable\": false, \"avg\": false, \"current\": false, \"hideEmpty\": true, \"hideZero\": true, \"max\": false, \"min\": false, \"rightSide\": false, \"show\": true, \"sideWidth\": null, \"total\": false, \"values\": false }, \"lines\": true, \"linewidth\": 2, \"links\": [], \"nullPointMode\": \"null\", \"percentage\": false, \"pointradius\": 5, \"points\": false, \"renderer\": \"flot\", \"seriesOverrides\": [], \"spaceLength\": 10, \"span\": 6, \"stack\": false, \"steppedLine\": false, \"targets\": [ { \"expr\": \"histogram_quantile(0.99, sum(rate(http_server_request_duration_seconds_bucket{kubernetes_namespace=~\\\"$Namespace\\\",kubernetes_name=~\\\"$Deployment\\\"}[1m])) by (le))\", \"format\": \"time_series\", \"instant\": false, \"interval\": \"\", \"intervalFactor\": 2, \"legendFormat\": \"99th percentile \", \"refId\": \"C\" }, { \"expr\": \"histogram_quantile(0.90, sum(rate(http_server_request_duration_seconds_bucket{kubernetes_namespace=~\\\"$Namespace\\\",kubernetes_name=~\\\"$Deployment\\\"}[1m])) by (le))\", \"format\": \"time_series\", \"intervalFactor\": 2, \"legendFormat\": \"90th percentile \", \"refId\": \"B\" }, { \"expr\": \"histogram_quantile(0.80, sum(rate(http_server_request_duration_seconds_bucket{kubernetes_namespace=~\\\"$Namespace\\\",kubernetes_name=~\\\"$Deployment\\\"}[1m])) by (le))\", \"format\": \"time_series\", \"intervalFactor\": 2, \"legendFormat\": \"80th percentile \", \"refId\": \"A\" } ], \"thresholds\": [], \"timeFrom\": null, \"timeShift\": null, \"title\": \"Request duration percentiles\", \"tooltip\": { \"shared\": true, \"sort\": 0, \"value_type\": \"individual\" }, \"type\": \"graph\", \"xaxis\": { \"buckets\": null, \"mode\": \"time\", \"name\": null, \"show\": true, \"values\": [] }, \"yaxes\": [ { \"decimals\": null, \"format\": \"short\", \"label\": \"\", \"logBase\": 1, \"max\": null, \"min\": null, \"show\": true }, { \"format\": \"short\", \"label\": null, \"logBase\": 1, \"max\": null, \"min\": null, \"show\": false } ] }, { \"aliasColors\": {}, \"bars\": false, \"dashLength\": 10, \"dashes\": false, \"datasource\": \"${DS_PROMETHEUS}\", \"fill\": 5, \"id\": 44, \"legend\": { \"alignAsTable\": false, \"avg\": false, \"current\": false, \"hideEmpty\": false, \"hideZero\": false, \"max\": false, \"min\": false, \"rightSide\": false, \"show\": true, \"sideWidth\": null, \"total\": false, \"values\": false }, \"lines\": true, \"linewidth\": 2, \"links\": [], \"nullPointMode\": \"null as zero\", \"percentage\": false, \"pointradius\": 5, \"points\": false, \"renderer\": \"flot\", \"seriesOverrides\": [], \"spaceLength\": 10, \"span\": 6, \"stack\": false, \"steppedLine\": false, \"targets\": [ { \"expr\": \"sum(rate(http_server_requests_total{kubernetes_namespace=~\\\"$Namespace\\\",kubernetes_name=~\\\"$Deployment\\\",code=~\\\"^2.*\\\"}[1m])) by (code) * 60\", \"format\": \"time_series\", \"intervalFactor\": 2, \"legendFormat\": \"2xx\", \"refId\": \"A\" }, { \"expr\": \"sum(rate(http_server_requests_total{kubernetes_namespace=~\\\"$Namespace\\\",kubernetes_name=~\\\"$Deployment\\\",code=~\\\"^3.*\\\"}[1m])) by (code) * 60\", \"format\": \"time_series\", \"intervalFactor\": 2, \"legendFormat\": \"3xx\", \"refId\": \"B\" }, { \"expr\": \"sum(rate(http_server_requests_total{kubernetes_namespace=~\\\"$Namespace\\\",kubernetes_name=~\\\"$Deployment\\\",code=~\\\"^4.*\\\"}[1m])) by (code) * 60\", \"format\": \"time_series\", \"intervalFactor\": 2, \"legendFormat\": \"4xx\", \"refId\": \"C\" }, { \"expr\": \"sum(rate(http_server_requests_total{kubernetes_namespace=~\\\"$Namespace\\\",kubernetes_name=~\\\"$Deployment\\\",code=~\\\"^5.*\\\"}[1m])) by (code) * 60\", \"format\": \"time_series\", \"intervalFactor\": 2, \"legendFormat\": \"5xx\", \"refId\": \"D\" } ], \"thresholds\": [], \"timeFrom\": null, \"timeShift\": null, \"title\": \"Requests per minute\", \"tooltip\": { \"shared\": true, \"sort\": 0, \"value_type\": \"individual\" }, \"type\": \"graph\", \"xaxis\": { \"buckets\": null, \"mode\": \"time\", \"name\": null, \"show\": true, \"values\": [] }, \"yaxes\": [ { \"format\": \"short\", \"label\": null, \"logBase\": 1, \"max\": null, \"min\": null, \"show\": true }, { \"format\": \"short\", \"label\": null, \"logBase\": 1, \"max\": null, \"min\": null, \"show\": false } ] } ], \"repeat\": null, \"repeatIteration\": null, \"repeatRowId\": null, \"showTitle\": false, \"title\": \"Requests\", \"titleSize\": \"h6\" }, { \"collapse\": false, \"height\": 310, \"panels\": [ { \"aliasColors\": {}, \"bars\": false, \"dashLength\": 10, \"dashes\": false, \"datasource\": \"${DS_PROMETHEUS}\", \"decimals\": 3, \"editable\": true, \"error\": false, \"fill\": 0, \"grid\": {}, \"height\": \"\", \"id\": 17, \"legend\": { \"alignAsTable\": true, \"avg\": false, \"current\": false, \"max\": false, \"min\": false, \"rightSide\": true, \"show\": false, \"sort\": \"current\", \"sortDesc\": true, \"total\": false, \"values\": false }, \"lines\": true, \"linewidth\": 2, \"links\": [], \"nullPointMode\": \"connected\", \"percentage\": false, \"pointradius\": 5, \"points\": false, \"renderer\": \"flot\", \"seriesOverrides\": [], \"spaceLength\": 10, \"span\": 5, \"stack\": false, \"steppedLine\": false, \"targets\": [ { \"expr\": \"sum (rate (container_cpu_usage_seconds_total{image!=\\\"\\\",name=~\\\"^k8s_.*\\\",pod_name=~\\\"^($Deployment).*$\\\",namespace=~\\\"^($Namespace).*$\\\"}[1m])) by (pod_name)\", \"format\": \"time_series\", \"interval\": \"10s\", \"intervalFactor\": 1, \"legendFormat\": \"{{ pod_name }}\", \"metric\": \"container_cpu\", \"refId\": \"A\", \"step\": 10 } ], \"thresholds\": [], \"timeFrom\": null, \"timeShift\": null, \"title\": \"CPU usage\", \"tooltip\": { \"msResolution\": true, \"shared\": true, \"sort\": 2, \"value_type\": \"cumulative\" }, \"transparent\": false, \"type\": \"graph\", \"xaxis\": { \"buckets\": null, \"mode\": \"time\", \"name\": null, \"show\": true, \"values\": [] }, \"yaxes\": [ { \"format\": \"none\", \"label\": \"cores\", \"logBase\": 1, \"max\": null, \"min\": null, \"show\": true }, { \"format\": \"short\", \"label\": null, \"logBase\": 1, \"max\": null, \"min\": null, \"show\": false } ] }, { \"aliasColors\": {}, \"bars\": false, \"dashLength\": 10, \"dashes\": false, \"datasource\": \"${DS_PROMETHEUS}\", \"decimals\": 2, \"editable\": true, \"error\": false, \"fill\": 0, \"grid\": {}, \"height\": \"\", \"id\": 25, \"legend\": { \"alignAsTable\": true, \"avg\": false, \"current\": false, \"max\": false, \"min\": false, \"rightSide\": true, \"show\": false, \"sideWidth\": 200, \"sort\": \"current\", \"sortDesc\": true, \"total\": false, \"values\": false }, \"lines\": true, \"linewidth\": 2, \"links\": [], \"nullPointMode\": \"connected\", \"percentage\": false, \"pointradius\": 5, \"points\": false, \"renderer\": \"flot\", \"seriesOverrides\": [], \"spaceLength\": 10, \"span\": 5, \"stack\": false, \"steppedLine\": false, \"targets\": [ { \"expr\": \"sum (container_memory_working_set_bytes{image!=\\\"\\\",name=~\\\"^k8s_.*\\\",pod_name=~\\\"^($Deployment).*$\\\",namespace=~\\\"^($Namespace).*$\\\"}) by (pod_name)\", \"format\": \"time_series\", \"interval\": \"10s\", \"intervalFactor\": 1, \"legendFormat\": \"{{ pod_name }}\", \"metric\": \"container_memory_usage:sort_desc\", \"refId\": \"A\", \"step\": 10 } ], \"thresholds\": [], \"timeFrom\": null, \"timeShift\": null, \"title\": \"Memory usage\", \"tooltip\": { \"msResolution\": false, \"shared\": true, \"sort\": 2, \"value_type\": \"cumulative\" }, \"type\": \"graph\", \"xaxis\": { \"buckets\": null, \"mode\": \"time\", \"name\": null, \"show\": true, \"values\": [] }, \"yaxes\": [ { \"format\": \"bytes\", \"label\": null, \"logBase\": 1, \"max\": null, \"min\": null, \"show\": true }, { \"format\": \"short\", \"label\": null, \"logBase\": 1, \"max\": null, \"min\": null, \"show\": false } ] }, { \"cacheTimeout\": null, \"colorBackground\": false, \"colorValue\": false, \"colors\": [ \"#bf1b00\", \"rgba(237, 129, 40, 0.89)\", \"rgba(50, 172, 45, 0.97)\" ], \"datasource\": \"${DS_PROMETHEUS}\", \"editable\": true, \"error\": false, \"format\": \"percent\", \"gauge\": { \"maxValue\": 100, \"minValue\": 0, \"show\": true, \"thresholdLabels\": false, \"thresholdMarkers\": true }, \"height\": \"200\", \"id\": 47, \"interval\": null, \"links\": [], \"mappingType\": 1, \"mappingTypes\": [ { \"name\": \"value to text\", \"value\": 1 }, { \"name\": \"range to text\", \"value\": 2 } ], \"maxDataPoints\": 100, \"nullPointMode\": \"connected\", \"nullText\": null, \"postfix\": \"\", \"postfixFontSize\": \"50%\", \"prefix\": \"\", \"prefixFontSize\": \"50%\", \"rangeMaps\": [ { \"from\": \"null\", \"text\": \"N/A\", \"to\": \"null\" } ], \"span\": 2, \"sparkline\": { \"fillColor\": \"rgba(31, 118, 189, 0.18)\", \"full\": false, \"lineColor\": \"rgb(31, 120, 193)\", \"show\": false }, \"tableColumn\": \"\", \"targets\": [ { \"expr\": \"sum(kube_deployment_status_replicas_available{deployment=~\\\"^($Deployment).*$\\\",namespace=~\\\"^($Namespace).*$\\\"}) / sum(kube_deployment_status_replicas{deployment=~\\\"^($Deployment).*$\\\",namespace=~\\\"^($Namespace).*$\\\"}) * 100\", \"format\": \"time_series\", \"intervalFactor\": 2, \"refId\": \"A\", \"step\": 40 } ], \"thresholds\": \"90,97\", \"title\": \"Replicas\", \"type\": \"singlestat\", \"valueFontSize\": \"80%\", \"valueMaps\": [ { \"op\": \"=\", \"text\": \"N/A\", \"value\": \"null\" } ], \"valueName\": \"avg\" }, { \"cacheTimeout\": null, \"colorBackground\": false, \"colorValue\": false, \"colors\": [ \"rgba(245, 54, 54, 0.9)\", \"rgba(237, 129, 40, 0.89)\", \"rgba(50, 172, 45, 0.97)\" ], \"datasource\": \"${DS_PROMETHEUS}\", \"editable\": true, \"error\": false, \"format\": \"none\", \"gauge\": { \"maxValue\": 100, \"minValue\": 0, \"show\": false, \"thresholdLabels\": false, \"thresholdMarkers\": true }, \"height\": \"100px\", \"id\": 48, \"interval\": null, \"links\": [], \"mappingType\": 1, \"mappingTypes\": [ { \"name\": \"value to text\", \"value\": 1 }, { \"name\": \"range to text\", \"value\": 2 } ], \"maxDataPoints\": 100, \"nullPointMode\": \"connected\", \"nullText\": null, \"postfix\": \"\", \"postfixFontSize\": \"50%\", \"prefix\": \"\", \"prefixFontSize\": \"50%\", \"rangeMaps\": [ { \"from\": \"null\", \"text\": \"N/A\", \"to\": \"null\" } ], \"span\": 1, \"sparkline\": { \"fillColor\": \"rgba(31, 118, 189, 0.18)\", \"full\": false, \"lineColor\": \"rgb(31, 120, 193)\", \"show\": false }, \"tableColumn\": \"\", \"targets\": [ { \"expr\": \"sum(kube_deployment_status_replicas_available{deployment=~\\\"^($Deployment).*$\\\",namespace=~\\\"^($Namespace).*$\\\"})\", \"format\": \"time_series\", \"intervalFactor\": 2, \"refId\": \"A\", \"step\": 40 } ], \"thresholds\": \"\", \"title\": \"Available\", \"type\": \"singlestat\", \"valueFontSize\": \"50%\", \"valueMaps\": [ { \"op\": \"=\", \"text\": \"N/A\", \"value\": \"null\" } ], \"valueName\": \"avg\" }, { \"cacheTimeout\": null, \"colorBackground\": false, \"colorValue\": false, \"colors\": [ \"rgba(245, 54, 54, 0.9)\", \"rgba(237, 129, 40, 0.89)\", \"rgba(50, 172, 45, 0.97)\" ], \"datasource\": \"${DS_PROMETHEUS}\", \"editable\": true, \"error\": false, \"format\": \"none\", \"gauge\": { \"maxValue\": 100, \"minValue\": 0, \"show\": false, \"thresholdLabels\": false, \"thresholdMarkers\": true }, \"height\": \"100px\", \"id\": 49, \"interval\": null, \"links\": [], \"mappingType\": 1, \"mappingTypes\": [ { \"name\": \"value to text\", \"value\": 1 }, { \"name\": \"range to text\", \"value\": 2 } ], \"maxDataPoints\": 100, \"nullPointMode\": \"connected\", \"nullText\": null, \"postfix\": \"\", \"postfixFontSize\": \"50%\", \"prefix\": \"\", \"prefixFontSize\": \"50%\", \"rangeMaps\": [ { \"from\": \"null\", \"text\": \"N/A\", \"to\": \"null\" } ], \"span\": 1, \"sparkline\": { \"fillColor\": \"rgba(31, 118, 189, 0.18)\", \"full\": false, \"lineColor\": \"rgb(31, 120, 193)\", \"show\": false }, \"tableColumn\": \"\", \"targets\": [ { \"expr\": \"sum(kube_deployment_status_replicas{deployment=~\\\"^($Deployment).*$\\\",namespace=~\\\"^($Namespace).*$\\\"})\", \"format\": \"time_series\", \"intervalFactor\": 2, \"refId\": \"A\", \"step\": 40 } ], \"thresholds\": \"\", \"title\": \"Total\", \"type\": \"singlestat\", \"valueFontSize\": \"50%\", \"valueMaps\": [ { \"op\": \"=\", \"text\": \"N/A\", \"value\": \"null\" } ], \"valueName\": \"avg\" } ], \"repeat\": null, \"repeatIteration\": null, \"repeatRowId\": null, \"showTitle\": false, \"title\": \"CPU and memory usage\", \"titleSize\": \"h6\" }, { \"collapse\": false, \"height\": 207, \"panels\": [ { \"aliasColors\": { \"Too_many_4xx - development - contract\": \"#bf1b00\" }, \"bars\": false, \"dashLength\": 10, \"dashes\": false, \"datasource\": \"${DS_PROMETHEUS}\", \"fill\": 1, \"id\": 50, \"legend\": { \"alignAsTable\": true, \"avg\": false, \"current\": false, \"hideEmpty\": true, \"hideZero\": true, \"max\": false, \"min\": false, \"rightSide\": true, \"show\": true, \"total\": false, \"values\": false }, \"lines\": false, \"linewidth\": 1, \"links\": [], \"nullPointMode\": \"null\", \"percentage\": false, \"pointradius\": 8, \"points\": true, \"renderer\": \"flot\", \"seriesOverrides\": [], \"spaceLength\": 10, \"span\": 4, \"stack\": false, \"steppedLine\": false, \"targets\": [ { \"expr\": \"ALERTS{namespace=~\\\"^$Namespace$\\\"}\", \"format\": \"time_series\", \"intervalFactor\": 10, \"legendFormat\": \"{{ alertname }} - {{ namespace }} - {{ app }}\", \"refId\": \"A\" } ], \"thresholds\": [], \"timeFrom\": null, \"timeShift\": null, \"title\": \"Errors\", \"tooltip\": { \"shared\": true, \"sort\": 0, \"value_type\": \"individual\" }, \"type\": \"graph\", \"xaxis\": { \"buckets\": null, \"mode\": \"time\", \"name\": null, \"show\": true, \"values\": [] }, \"yaxes\": [ { \"format\": \"short\", \"label\": \"\", \"logBase\": 1, \"max\": \"1.5\", \"min\": \"0.5\", \"show\": false }, { \"format\": \"short\", \"label\": null, \"logBase\": 1, \"max\": null, \"min\": null, \"show\": false } ] }, { \"aliasColors\": {}, \"bars\": false, \"dashLength\": 10, \"dashes\": false, \"datasource\": \"${DS_PROMETHEUS}\", \"fill\": 0, \"id\": 45, \"legend\": { \"alignAsTable\": true, \"avg\": false, \"current\": false, \"hideEmpty\": true, \"hideZero\": true, \"max\": false, \"min\": false, \"rightSide\": true, \"show\": true, \"sideWidth\": null, \"sort\": \"avg\", \"sortDesc\": true, \"total\": false, \"values\": false }, \"lines\": true, \"linewidth\": 2, \"links\": [], \"nullPointMode\": \"null\", \"percentage\": false, \"pointradius\": 5, \"points\": false, \"renderer\": \"flot\", \"seriesOverrides\": [], \"spaceLength\": 10, \"span\": 4, \"stack\": false, \"steppedLine\": false, \"targets\": [ { \"expr\": \"topk(10,sum by (path)(rate(http_server_requests_total{kubernetes_namespace=~\\\"$Namespace\\\",kubernetes_name=~\\\"$Deployment\\\"}[1m]))) * 60\", \"format\": \"time_series\", \"intervalFactor\": 2, \"legendFormat\": \"{{ path }}\", \"refId\": \"A\" } ], \"thresholds\": [], \"timeFrom\": null, \"timeShift\": null, \"title\": \"Top 10 requested routes\", \"tooltip\": { \"shared\": true, \"sort\": 2, \"value_type\": \"individual\" }, \"type\": \"graph\", \"xaxis\": { \"buckets\": null, \"mode\": \"time\", \"name\": null, \"show\": true, \"values\": [] }, \"yaxes\": [ { \"format\": \"short\", \"label\": null, \"logBase\": 1, \"max\": null, \"min\": null, \"show\": true }, { \"format\": \"short\", \"label\": null, \"logBase\": 1, \"max\": null, \"min\": null, \"show\": true } ] }, { \"aliasColors\": {}, \"bars\": false, \"dashLength\": 10, \"dashes\": false, \"datasource\": \"${DS_PROMETHEUS}\", \"decimals\": 2, \"editable\": true, \"error\": false, \"fill\": 1, \"grid\": {}, \"id\": 16, \"legend\": { \"alignAsTable\": true, \"avg\": false, \"current\": false, \"max\": false, \"min\": false, \"rightSide\": true, \"show\": true, \"sideWidth\": 200, \"sort\": \"current\", \"sortDesc\": true, \"total\": false, \"values\": false }, \"lines\": true, \"linewidth\": 2, \"links\": [], \"nullPointMode\": \"connected\", \"percentage\": false, \"pointradius\": 5, \"points\": false, \"renderer\": \"flot\", \"seriesOverrides\": [], \"spaceLength\": 10, \"span\": 4, \"stack\": false, \"steppedLine\": false, \"targets\": [ { \"expr\": \"sum (rate (container_network_receive_bytes_total{image!=\\\"\\\",name=~\\\"^k8s_.*\\\",pod_name=~\\\"^($Deployment).*$\\\",namespace=~\\\"^($Namespace).*$\\\"}[1m])) by (pod_name)\", \"format\": \"time_series\", \"interval\": \"10s\", \"intervalFactor\": 1, \"legendFormat\": \"IN {{ pod_name }}\", \"metric\": \"network\", \"refId\": \"A\", \"step\": 10 }, { \"expr\": \"- sum (rate (container_network_transmit_bytes_total{image!=\\\"\\\",name=~\\\"^k8s_.*\\\",pod_name=~\\\"^($Deployment).*$\\\",namespace=~\\\"^($Namespace).*$\\\"}[1m])) by (pod_name)\", \"format\": \"time_series\", \"interval\": \"10s\", \"intervalFactor\": 1, \"legendFormat\": \"OUT {{ pod_name }}\", \"metric\": \"network\", \"refId\": \"B\", \"step\": 10 } ], \"thresholds\": [], \"timeFrom\": null, \"timeShift\": null, \"title\": \"Network I/O\", \"tooltip\": { \"msResolution\": false, \"shared\": true, \"sort\": 2, \"value_type\": \"cumulative\" }, \"type\": \"graph\", \"xaxis\": { \"buckets\": null, \"mode\": \"time\", \"name\": null, \"show\": true, \"values\": [] }, \"yaxes\": [ { \"format\": \"Bps\", \"label\": null, \"logBase\": 1, \"max\": null, \"min\": null, \"show\": true }, { \"format\": \"short\", \"label\": null, \"logBase\": 1, \"max\": null, \"min\": null, \"show\": false } ] } ], \"repeat\": null, \"repeatIteration\": null, \"repeatRowId\": null, \"showTitle\": false, \"title\": \"Network\", \"titleSize\": \"h6\" } ], \"schemaVersion\": 14, \"style\": \"dark\", \"tags\": [ \"app\" ], \"templating\": { \"list\": [ { \"allValue\": \".*\", \"current\": {}, \"datasource\": \"${DS_PROMETHEUS}\", \"hide\": 0, \"includeAll\": true, \"label\": null, \"multi\": false, \"name\": \"Node\", \"options\": [], \"query\": \"label_values(kubernetes_io_hostname)\", \"refresh\": 1, \"regex\": \"\", \"sort\": 0, \"tagValuesQuery\": \"\", \"tags\": [], \"tagsQuery\": \"\", \"type\": \"query\", \"useTags\": false }, { \"allValue\": null, \"current\": {}, \"datasource\": \"${DS_PROMETHEUS}\", \"hide\": 0, \"includeAll\": true, \"label\": null, \"multi\": false, \"name\": \"Namespace\", \"options\": [], \"query\": \"label_values({namespace !~ \\\"default|kube-system|kube-public|.{0}\\\"}, namespace)\", \"refresh\": 1, \"regex\": \"\", \"sort\": 0, \"tagValuesQuery\": \"\", \"tags\": [], \"tagsQuery\": \"\", \"type\": \"query\", \"useTags\": false }, { \"allValue\": \"\", \"current\": {}, \"datasource\": \"${DS_PROMETHEUS}\", \"hide\": 0, \"includeAll\": true, \"label\": null, \"multi\": true, \"name\": \"Deployment\", \"options\": [], \"query\": \"label_values({namespace =~ \\\"$Namespace\\\", deployment =~ \\\".+\\\"}, deployment)\", \"refresh\": 1, \"regex\": \"\", \"sort\": 0, \"tagValuesQuery\": \"\", \"tags\": [], \"tagsQuery\": \"\", \"type\": \"query\", \"useTags\": false } ] }, \"time\": { \"from\": \"now-15m\", \"to\": \"now\" }, \"timepicker\": { \"refresh_intervals\": [ \"5s\", \"10s\", \"30s\", \"1m\", \"5m\", \"15m\", \"30m\", \"1h\", \"2h\", \"1d\" ], \"time_options\": [ \"5m\", \"15m\", \"1h\", \"6h\", \"12h\", \"24h\", \"2d\", \"7d\", \"30d\" ] }, \"timezone\": \"browser\", \"title\": \"Kubernetes Web App metrics\", \"version\": 30} 也可以针对K8s的各种类型资源进行: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384{ \"__inputs\": [ { \"name\": \"DS_PROMETHEUS\", \"label\": \"prometheus\", \"description\": \"\", \"type\": \"datasource\", \"pluginId\": \"prometheus\", \"pluginName\": \"Prometheus\" } ], \"__requires\": [ { \"type\": \"grafana\", \"id\": \"grafana\", \"name\": \"Grafana\", \"version\": \"5.1.2\" }, { \"type\": \"panel\", \"id\": \"graph\", \"name\": \"Graph\", \"version\": \"5.0.0\" }, { \"type\": \"datasource\", \"id\": \"prometheus\", \"name\": \"Prometheus\", \"version\": \"5.0.0\" }, { \"type\": \"panel\", \"id\": \"singlestat\", \"name\": \"Singlestat\", \"version\": \"5.0.0\" } ], \"annotations\": { \"list\": [ { \"builtIn\": 1, \"datasource\": \"-- Grafana --\", \"enable\": true, \"hide\": true, \"iconColor\": \"rgba(0, 211, 255, 1)\", \"name\": \"Annotations & Alerts\", \"type\": \"dashboard\" } ] }, \"description\": \"Monitors Kubernetes deployments in cluster using Prometheus. Shows overall cluster CPU / Memory of deployments, replicas in each deployment. Uses Kube state metrics and cAdvisor metrics (741)\", \"editable\": true, \"gnetId\": 8588, \"graphTooltip\": 0, \"id\": null, \"iteration\": 1540469214881, \"links\": [], \"panels\": [ { \"cacheTimeout\": null, \"colorBackground\": false, \"colorValue\": true, \"colors\": [ \"rgba(50, 172, 45, 0.97)\", \"rgba(237, 129, 40, 0.89)\", \"rgba(245, 54, 54, 0.9)\" ], \"datasource\": \"${DS_PROMETHEUS}\", \"editable\": true, \"error\": false, \"format\": \"percent\", \"gauge\": { \"maxValue\": 100, \"minValue\": 0, \"show\": true, \"thresholdLabels\": false, \"thresholdMarkers\": true }, \"gridPos\": { \"h\": 5, \"w\": 8, \"x\": 0, \"y\": 0 }, \"height\": \"180px\", \"id\": 1, \"interval\": null, \"links\": [], \"mappingType\": 1, \"mappingTypes\": [ { \"name\": \"value to text\", \"value\": 1 }, { \"name\": \"range to text\", \"value\": 2 } ], \"maxDataPoints\": 100, \"nullPointMode\": \"connected\", \"nullText\": null, \"postfix\": \"\", \"postfixFontSize\": \"50%\", \"prefix\": \"\", \"prefixFontSize\": \"50%\", \"rangeMaps\": [ { \"from\": \"null\", \"text\": \"N/A\", \"to\": \"null\" } ], \"sparkline\": { \"fillColor\": \"rgba(31, 118, 189, 0.18)\", \"full\": false, \"lineColor\": \"rgb(31, 120, 193)\", \"show\": false }, \"tableColumn\": \"\", \"targets\": [ { \"expr\": \"sum (container_memory_working_set_bytes{pod_name=~\\\"^$Deployment$Statefulset$Daemonset.*$\\\", kubernetes_io_hostname=~\\\"^$Node$\\\", pod_name!=\\\"\\\"}) / sum (kube_node_status_allocatable_memory_bytes{node=~\\\"^$Node.*$\\\"}) * 100\", \"format\": \"time_series\", \"interval\": \"10s\", \"intervalFactor\": 1, \"refId\": \"A\", \"step\": 900 } ], \"thresholds\": \"65, 90\", \"title\": \"Deployment memory usage\", \"transparent\": false, \"type\": \"singlestat\", \"valueFontSize\": \"80%\", \"valueMaps\": [ { \"op\": \"=\", \"text\": \"N/A\", \"value\": \"null\" } ], \"valueName\": \"current\" }, { \"cacheTimeout\": null, \"colorBackground\": false, \"colorValue\": true, \"colors\": [ \"rgba(50, 172, 45, 0.97)\", \"rgba(237, 129, 40, 0.89)\", \"rgba(245, 54, 54, 0.9)\" ], \"datasource\": \"${DS_PROMETHEUS}\", \"decimals\": 2, \"editable\": true, \"error\": false, \"format\": \"percent\", \"gauge\": { \"maxValue\": 100, \"minValue\": 0, \"show\": true, \"thresholdLabels\": false, \"thresholdMarkers\": true }, \"gridPos\": { \"h\": 5, \"w\": 8, \"x\": 8, \"y\": 0 }, \"height\": \"180px\", \"id\": 2, \"interval\": null, \"links\": [], \"mappingType\": 1, \"mappingTypes\": [ { \"name\": \"value to text\", \"value\": 1 }, { \"name\": \"range to text\", \"value\": 2 } ], \"maxDataPoints\": 100, \"nullPointMode\": \"connected\", \"nullText\": null, \"postfix\": \"\", \"postfixFontSize\": \"50%\", \"prefix\": \"\", \"prefixFontSize\": \"50%\", \"rangeMaps\": [ { \"from\": \"null\", \"text\": \"N/A\", \"to\": \"null\" } ], \"sparkline\": { \"fillColor\": \"rgba(31, 118, 189, 0.18)\", \"full\": false, \"lineColor\": \"rgb(31, 120, 193)\", \"show\": false }, \"tableColumn\": \"\", \"targets\": [ { \"expr\": \"sum (rate (container_cpu_usage_seconds_total{pod_name=~\\\"^$Deployment$Statefulset$Daemonset.*$\\\", kubernetes_io_hostname=~\\\"^$Node$\\\"}[2m])) / sum (machine_cpu_cores{kubernetes_io_hostname=~\\\"^$Node$\\\"}) * 100\", \"format\": \"time_series\", \"interval\": \"10s\", \"intervalFactor\": 1, \"refId\": \"A\", \"step\": 900 } ], \"thresholds\": \"65, 90\", \"title\": \"Deployment CPU usage\", \"type\": \"singlestat\", \"valueFontSize\": \"80%\", \"valueMaps\": [ { \"op\": \"=\", \"text\": \"N/A\", \"value\": \"null\" } ], \"valueName\": \"current\" }, { \"cacheTimeout\": null, \"colorBackground\": false, \"colorValue\": true, \"colors\": [ \"rgba(50, 172, 45, 0.97)\", \"rgba(237, 129, 40, 0.89)\", \"rgba(245, 54, 54, 0.9)\" ], \"datasource\": \"${DS_PROMETHEUS}\", \"editable\": true, \"error\": false, \"format\": \"percent\", \"gauge\": { \"maxValue\": 100, \"minValue\": 0, \"show\": true, \"thresholdLabels\": false, \"thresholdMarkers\": true }, \"gridPos\": { \"h\": 5, \"w\": 8, \"x\": 16, \"y\": 0 }, \"height\": \"180px\", \"id\": 3, \"interval\": null, \"links\": [], \"mappingType\": 1, \"mappingTypes\": [ { \"name\": \"value to text\", \"value\": 1 }, { \"name\": \"range to text\", \"value\": 2 } ], \"maxDataPoints\": 100, \"nullPointMode\": \"connected\", \"nullText\": null, \"postfix\": \"\", \"postfixFontSize\": \"50%\", \"prefix\": \"\", \"prefixFontSize\": \"50%\", \"rangeMaps\": [ { \"from\": \"null\", \"text\": \"N/A\", \"to\": \"null\" } ], \"sparkline\": { \"fillColor\": \"rgba(31, 118, 189, 0.18)\", \"full\": false, \"lineColor\": \"rgb(31, 120, 193)\", \"show\": false }, \"tableColumn\": \"\", \"targets\": [ { \"expr\": \"(((sum(kube_deployment_status_replicas{deployment=~\\\".*$Deployment$Statefulset$Daemonset\\\"}) or vector(0)) + (sum(kube_statefulset_replicas{statefulset=~\\\".*$Deployment$Statefulset$Daemonset\\\"}) or vector(0)) + (sum(kube_daemonset_status_desired_number_scheduled{daemonset=~\\\".*$Deployment$Statefulset$Daemonset\\\"}) or vector(0))) - ((sum(kube_deployment_status_replicas_available{deployment=~\\\".*$Deployment$Statefulset$Daemonset\\\"}) or vector(0)) + (sum(kube_statefulset_status_replicas{statefulset=~\\\".*$Deployment$Statefulset$Daemonset\\\"}) or vector(0)) + (sum(kube_daemonset_status_number_ready{daemonset=~\\\".*$Deployment$Statefulset$Daemonset\\\"}) or vector(0)))) / ((sum(kube_deployment_status_replicas{deployment=~\\\".*$Deployment$Statefulset$Daemonset\\\"}) or vector(0)) + (sum(kube_statefulset_replicas{statefulset=~\\\".*$Deployment$Statefulset$Daemonset\\\"}) or vector(0)) + (sum(kube_daemonset_status_desired_number_scheduled{daemonset=~\\\".*$Deployment$Statefulset$Daemonset\\\"}) or vector(0))) * 100\", \"format\": \"time_series\", \"intervalFactor\": 2, \"refId\": \"A\", \"step\": 1800 } ], \"thresholds\": \"1,30\", \"title\": \"Unavailable Replicas\", \"type\": \"singlestat\", \"valueFontSize\": \"80%\", \"valueMaps\": [ { \"op\": \"=\", \"text\": \"N/A\", \"value\": \"null\" } ], \"valueName\": \"current\" }, { \"cacheTimeout\": null, \"colorBackground\": false, \"colorValue\": false, \"colors\": [ \"rgba(245, 54, 54, 0.9)\", \"rgba(237, 129, 40, 0.89)\", \"rgba(50, 172, 45, 0.97)\" ], \"datasource\": \"${DS_PROMETHEUS}\", \"editable\": true, \"error\": false, \"format\": \"bytes\", \"gauge\": { \"maxValue\": 100, \"minValue\": 0, \"show\": false, \"thresholdLabels\": false, \"thresholdMarkers\": true }, \"gridPos\": { \"h\": 3, \"w\": 4, \"x\": 0, \"y\": 5 }, \"height\": \"100px\", \"id\": 4, \"interval\": null, \"links\": [], \"mappingType\": 1, \"mappingTypes\": [ { \"name\": \"value to text\", \"value\": 1 }, { \"name\": \"range to text\", \"value\": 2 } ], \"maxDataPoints\": 100, \"nullPointMode\": \"connected\", \"nullText\": null, \"postfix\": \"\", \"postfixFontSize\": \"50%\", \"prefix\": \"\", \"prefixFontSize\": \"50%\", \"rangeMaps\": [ { \"from\": \"null\", \"text\": \"N/A\", \"to\": \"null\" } ], \"sparkline\": { \"fillColor\": \"rgba(31, 118, 189, 0.18)\", \"full\": false, \"lineColor\": \"rgb(31, 120, 193)\", \"show\": false }, \"tableColumn\": \"\", \"targets\": [ { \"expr\": \"sum (container_memory_working_set_bytes{pod_name=~\\\"^$Deployment$Statefulset$Daemonset.*$\\\", kubernetes_io_hostname=~\\\"^$Node$\\\", pod_name!=\\\"\\\"})\", \"format\": \"time_series\", \"intervalFactor\": 2, \"refId\": \"A\", \"step\": 1800 } ], \"thresholds\": \"\", \"title\": \"Used\", \"type\": \"singlestat\", \"valueFontSize\": \"50%\", \"valueMaps\": [ { \"op\": \"=\", \"text\": \"N/A\", \"value\": \"null\" } ], \"valueName\": \"current\" }, { \"cacheTimeout\": null, \"colorBackground\": false, \"colorValue\": false, \"colors\": [ \"rgba(245, 54, 54, 0.9)\", \"rgba(237, 129, 40, 0.89)\", \"rgba(50, 172, 45, 0.97)\" ], \"datasource\": \"${DS_PROMETHEUS}\", \"editable\": true, \"error\": false, \"format\": \"bytes\", \"gauge\": { \"maxValue\": 100, \"minValue\": 0, \"show\": false, \"thresholdLabels\": false, \"thresholdMarkers\": true }, \"gridPos\": { \"h\": 3, \"w\": 4, \"x\": 4, \"y\": 5 }, \"height\": \"100px\", \"id\": 5, \"interval\": null, \"links\": [], \"mappingType\": 1, \"mappingTypes\": [ { \"name\": \"value to text\", \"value\": 1 }, { \"name\": \"range to text\", \"value\": 2 } ], \"maxDataPoints\": 100, \"nullPointMode\": \"connected\", \"nullText\": null, \"postfix\": \"\", \"postfixFontSize\": \"50%\", \"prefix\": \"\", \"prefixFontSize\": \"50%\", \"rangeMaps\": [ { \"from\": \"null\", \"text\": \"N/A\", \"to\": \"null\" } ], \"sparkline\": { \"fillColor\": \"rgba(31, 118, 189, 0.18)\", \"full\": false, \"lineColor\": \"rgb(31, 120, 193)\", \"show\": false }, \"tableColumn\": \"\", \"targets\": [ { \"expr\": \"sum (kube_node_status_allocatable_memory_bytes{node=~\\\"^$Node.*$\\\"})\", \"format\": \"time_series\", \"intervalFactor\": 2, \"refId\": \"A\", \"step\": 1800 } ], \"thresholds\": \"\", \"title\": \"Total\", \"type\": \"singlestat\", \"valueFontSize\": \"50%\", \"valueMaps\": [ { \"op\": \"=\", \"text\": \"N/A\", \"value\": \"null\" } ], \"valueName\": \"current\" }, { \"cacheTimeout\": null, \"colorBackground\": false, \"colorValue\": false, \"colors\": [ \"rgba(245, 54, 54, 0.9)\", \"rgba(237, 129, 40, 0.89)\", \"rgba(50, 172, 45, 0.97)\" ], \"datasource\": \"${DS_PROMETHEUS}\", \"editable\": true, \"error\": false, \"format\": \"none\", \"gauge\": { \"maxValue\": 100, \"minValue\": 0, \"show\": false, \"thresholdLabels\": false, \"thresholdMarkers\": true }, \"gridPos\": { \"h\": 3, \"w\": 4, \"x\": 8, \"y\": 5 }, \"height\": \"100px\", \"id\": 6, \"interval\": null, \"links\": [], \"mappingType\": 1, \"mappingTypes\": [ { \"name\": \"value to text\", \"value\": 1 }, { \"name\": \"range to text\", \"value\": 2 } ], \"maxDataPoints\": 100, \"nullPointMode\": \"connected\", \"nullText\": null, \"postfix\": \" cores\", \"postfixFontSize\": \"50%\", \"prefix\": \"\", \"prefixFontSize\": \"50%\", \"rangeMaps\": [ { \"from\": \"null\", \"text\": \"N/A\", \"to\": \"null\" } ], \"sparkline\": { \"fillColor\": \"rgba(31, 118, 189, 0.18)\", \"full\": false, \"lineColor\": \"rgb(31, 120, 193)\", \"show\": false }, \"tableColumn\": \"\", \"targets\": [ { \"expr\": \"sum (rate (container_cpu_usage_seconds_total{pod_name=~\\\"^$Deployment$Statefulset$Daemonset.*$\\\", kubernetes_io_hostname=~\\\"^$Node$\\\"}[1m]))\", \"format\": \"time_series\", \"intervalFactor\": 2, \"refId\": \"A\", \"step\": 1800 } ], \"thresholds\": \"\", \"title\": \"Used\", \"type\": \"singlestat\", \"valueFontSize\": \"50%\", \"valueMaps\": [ { \"op\": \"=\", \"text\": \"N/A\", \"value\": \"null\" } ], \"valueName\": \"current\" }, { \"cacheTimeout\": null, \"colorBackground\": false, \"colorValue\": false, \"colors\": [ \"rgba(245, 54, 54, 0.9)\", \"rgba(237, 129, 40, 0.89)\", \"rgba(50, 172, 45, 0.97)\" ], \"datasource\": \"${DS_PROMETHEUS}\", \"editable\": true, \"error\": false, \"format\": \"none\", \"gauge\": { \"maxValue\": 100, \"minValue\": 0, \"show\": false, \"thresholdLabels\": false, \"thresholdMarkers\": true }, \"gridPos\": { \"h\": 3, \"w\": 4, \"x\": 12, \"y\": 5 }, \"height\": \"100px\", \"id\": 7, \"interval\": null, \"links\": [], \"mappingType\": 1, \"mappingTypes\": [ { \"name\": \"value to text\", \"value\": 1 }, { \"name\": \"range to text\", \"value\": 2 } ], \"maxDataPoints\": 100, \"nullPointMode\": \"connected\", \"nullText\": null, \"postfix\": \" cores\", \"postfixFontSize\": \"50%\", \"prefix\": \"\", \"prefixFontSize\": \"50%\", \"rangeMaps\": [ { \"from\": \"null\", \"text\": \"N/A\", \"to\": \"null\" } ], \"sparkline\": { \"fillColor\": \"rgba(31, 118, 189, 0.18)\", \"full\": false, \"lineColor\": \"rgb(31, 120, 193)\", \"show\": false }, \"tableColumn\": \"\", \"targets\": [ { \"expr\": \"sum (machine_cpu_cores{kubernetes_io_hostname=~\\\"^$Node$\\\"})\", \"intervalFactor\": 2, \"refId\": \"A\", \"step\": 1800 } ], \"thresholds\": \"\", \"title\": \"Total\", \"type\": \"singlestat\", \"valueFontSize\": \"50%\", \"valueMaps\": [ { \"op\": \"=\", \"text\": \"N/A\", \"value\": \"null\" } ], \"valueName\": \"avg\" }, { \"cacheTimeout\": null, \"colorBackground\": false, \"colorValue\": false, \"colors\": [ \"rgba(245, 54, 54, 0.9)\", \"rgba(237, 129, 40, 0.89)\", \"rgba(50, 172, 45, 0.97)\" ], \"datasource\": \"${DS_PROMETHEUS}\", \"editable\": true, \"error\": false, \"format\": \"none\", \"gauge\": { \"maxValue\": 100, \"minValue\": 0, \"show\": false, \"thresholdLabels\": false, \"thresholdMarkers\": true }, \"gridPos\": { \"h\": 3, \"w\": 4, \"x\": 16, \"y\": 5 }, \"height\": \"100px\", \"id\": 8, \"interval\": null, \"links\": [], \"mappingType\": 1, \"mappingTypes\": [ { \"name\": \"value to text\", \"value\": 1 }, { \"name\": \"range to text\", \"value\": 2 } ], \"maxDataPoints\": 100, \"nullPointMode\": \"connected\", \"nullText\": null, \"postfix\": \"\", \"postfixFontSize\": \"50%\", \"prefix\": \"\", \"prefixFontSize\": \"50%\", \"rangeMaps\": [ { \"from\": \"null\", \"text\": \"N/A\", \"to\": \"null\" } ], \"sparkline\": { \"fillColor\": \"rgba(31, 118, 189, 0.18)\", \"full\": false, \"lineColor\": \"rgb(31, 120, 193)\", \"show\": false }, \"tableColumn\": \"\", \"targets\": [ { \"expr\": \"(sum(kube_deployment_status_replicas_available{deployment=~\\\".*$Deployment$Statefulset$Daemonset\\\"}) or vector(0)) + (sum(kube_statefulset_status_replicas{statefulset=~\\\".*$Deployment$Statefulset$Daemonset\\\"}) or vector(0)) + (sum(kube_daemonset_status_number_ready{daemonset=~\\\".*$Deployment$Statefulset$Daemonset\\\"}) or vector(0))\", \"format\": \"time_series\", \"intervalFactor\": 2, \"refId\": \"A\", \"step\": 1800 } ], \"thresholds\": \"\", \"title\": \"Available (cluster)\", \"type\": \"singlestat\", \"valueFontSize\": \"50%\", \"valueMaps\": [ { \"op\": \"=\", \"text\": \"N/A\", \"value\": \"null\" } ], \"valueName\": \"current\" }, { \"cacheTimeout\": null, \"colorBackground\": false, \"colorValue\": false, \"colors\": [ \"rgba(245, 54, 54, 0.9)\", \"rgba(237, 129, 40, 0.89)\", \"rgba(50, 172, 45, 0.97)\" ], \"datasource\": \"${DS_PROMETHEUS}\", \"editable\": true, \"error\": false, \"format\": \"none\", \"gauge\": { \"maxValue\": 100, \"minValue\": 0, \"show\": false, \"thresholdLabels\": false, \"thresholdMarkers\": true }, \"gridPos\": { \"h\": 3, \"w\": 4, \"x\": 20, \"y\": 5 }, \"height\": \"100px\", \"id\": 9, \"interval\": null, \"links\": [], \"mappingType\": 1, \"mappingTypes\": [ { \"name\": \"value to text\", \"value\": 1 }, { \"name\": \"range to text\", \"value\": 2 } ], \"maxDataPoints\": 100, \"nullPointMode\": \"connected\", \"nullText\": null, \"postfix\": \"\", \"postfixFontSize\": \"50%\", \"prefix\": \"\", \"prefixFontSize\": \"50%\", \"rangeMaps\": [ { \"from\": \"null\", \"text\": \"N/A\", \"to\": \"null\" } ], \"sparkline\": { \"fillColor\": \"rgba(31, 118, 189, 0.18)\", \"full\": false, \"lineColor\": \"rgb(31, 120, 193)\", \"show\": false }, \"tableColumn\": \"\", \"targets\": [ { \"expr\": \"(sum(kube_deployment_status_replicas{deployment=~\\\".*$Deployment$Statefulset$Daemonset\\\"}) or vector(0)) + (sum(kube_statefulset_replicas{statefulset=~\\\".*$Deployment$Statefulset$Daemonset\\\"}) or vector(0)) + (sum(kube_daemonset_status_desired_number_scheduled{daemonset=~\\\".*$Deployment$Statefulset$Daemonset\\\"}) or vector(0))\", \"format\": \"time_series\", \"intervalFactor\": 2, \"legendFormat\": \"{{ $Daemonset }}\", \"refId\": \"A\", \"step\": 1800 } ], \"thresholds\": \"\", \"title\": \"Total (cluster)\", \"type\": \"singlestat\", \"valueFontSize\": \"50%\", \"valueMaps\": [ { \"op\": \"=\", \"text\": \"N/A\", \"value\": \"null\" } ], \"valueName\": \"current\" }, { \"aliasColors\": {}, \"bars\": false, \"dashLength\": 10, \"dashes\": false, \"datasource\": \"${DS_PROMETHEUS}\", \"decimals\": 3, \"editable\": true, \"error\": false, \"fill\": 0, \"grid\": {}, \"gridPos\": { \"h\": 11, \"w\": 24, \"x\": 0, \"y\": 8 }, \"height\": \"\", \"id\": 10, \"legend\": { \"alignAsTable\": true, \"avg\": false, \"current\": true, \"hideEmpty\": false, \"hideZero\": false, \"max\": true, \"min\": false, \"rightSide\": true, \"show\": true, \"sideWidth\": null, \"sort\": \"current\", \"sortDesc\": true, \"total\": false, \"values\": true }, \"lines\": true, \"linewidth\": 2, \"links\": [], \"nullPointMode\": \"connected\", \"percentage\": false, \"pointradius\": 5, \"points\": false, \"renderer\": \"flot\", \"seriesOverrides\": [ { \"alias\": \"/avlbl.*/\", \"yaxis\": 2 } ], \"spaceLength\": 10, \"stack\": false, \"steppedLine\": false, \"targets\": [ { \"expr\": \"sum (rate (container_cpu_usage_seconds_total{image!=\\\"\\\",name=~\\\"^k8s_.*\\\",io_kubernetes_container_name!=\\\"POD\\\",pod_name=~\\\"^$Deployment$Statefulset$Daemonset.*$\\\",kubernetes_io_hostname=~\\\"^$Node$\\\"}[1m])) by (pod_name,kubernetes_io_hostname)\", \"format\": \"time_series\", \"hide\": false, \"interval\": \"10s\", \"intervalFactor\": 1, \"legendFormat\": \"real: {{ kubernetes_io_hostname }} | {{ pod_name }} \", \"metric\": \"container_cpu\", \"refId\": \"A\", \"step\": 60 }, { \"expr\": \"sum (kube_pod_container_resource_requests_cpu_cores{pod=~\\\"^$Deployment$Statefulset$Daemonset.*$\\\",node=~\\\"^$Node$\\\"}) by (pod,node)\", \"format\": \"time_series\", \"hide\": false, \"intervalFactor\": 2, \"legendFormat\": \"rqst: {{ node }} | {{ pod }}\", \"refId\": \"B\", \"step\": 120 }, { \"expr\": \"sum ((kube_node_status_allocatable_cpu_cores{node=~\\\"^$Node$\\\"})) by (node)\", \"format\": \"time_series\", \"hide\": true, \"intervalFactor\": 2, \"legendFormat\": \"avlbl: {{ node }}\", \"refId\": \"C\", \"step\": 30 } ], \"thresholds\": [], \"timeFrom\": null, \"timeShift\": null, \"title\": \"CPU usage\", \"tooltip\": { \"msResolution\": true, \"shared\": true, \"sort\": 2, \"value_type\": \"cumulative\" }, \"type\": \"graph\", \"xaxis\": { \"buckets\": null, \"mode\": \"time\", \"name\": null, \"show\": true, \"values\": [] }, \"yaxes\": [ { \"format\": \"none\", \"label\": \"cores\", \"logBase\": 1, \"max\": null, \"min\": null, \"show\": true }, { \"format\": \"short\", \"label\": null, \"logBase\": 1, \"max\": null, \"min\": null, \"show\": true } ], \"yaxis\": { \"align\": false, \"alignLevel\": null } }, { \"aliasColors\": {}, \"bars\": false, \"dashLength\": 10, \"dashes\": false, \"datasource\": \"${DS_PROMETHEUS}\", \"decimals\": 2, \"editable\": true, \"error\": false, \"fill\": 0, \"grid\": {}, \"gridPos\": { \"h\": 13, \"w\": 24, \"x\": 0, \"y\": 19 }, \"id\": 11, \"legend\": { \"alignAsTable\": true, \"avg\": false, \"current\": true, \"max\": true, \"min\": false, \"rightSide\": true, \"show\": true, \"sideWidth\": null, \"sort\": \"current\", \"sortDesc\": true, \"total\": false, \"values\": true }, \"lines\": true, \"linewidth\": 2, \"links\": [], \"nullPointMode\": \"connected\", \"percentage\": false, \"pointradius\": 5, \"points\": false, \"renderer\": \"flot\", \"seriesOverrides\": [ { \"alias\": \"/^avlbl.*$/\", \"yaxis\": 2 } ], \"spaceLength\": 10, \"stack\": false, \"steppedLine\": false, \"targets\": [ { \"expr\": \"sum (container_memory_working_set_bytes{id!=\\\"/\\\",pod_name=~\\\"^$Deployment$Statefulset$Daemonset.*$\\\",kubernetes_io_hostname=~\\\"^$Node$\\\"}) by (pod_name,kubernetes_io_hostname)\", \"format\": \"time_series\", \"hide\": false, \"interval\": \"10s\", \"intervalFactor\": 1, \"legendFormat\": \"real: {{kubernetes_io_hostname }} | {{ pod_name }}\", \"metric\": \"container_memory_usage:sort_desc\", \"refId\": \"A\", \"step\": 60 }, { \"expr\": \"sum ((kube_pod_container_resource_requests_memory_bytes{pod=~\\\"^$Deployment$Statefulset$Daemonset.*$\\\",node=~\\\"^$Node$\\\"})) by (pod,node)\", \"format\": \"time_series\", \"intervalFactor\": 2, \"legendFormat\": \"rqst: {{ node }} | {{ pod }}\", \"refId\": \"B\", \"step\": 120 }, { \"expr\": \"sum ((kube_node_status_allocatable_memory_bytes{node=~\\\"^$Node$\\\"})) by (node)\", \"format\": \"time_series\", \"hide\": true, \"intervalFactor\": 2, \"legendFormat\": \"avlbl: {{ node }}\", \"refId\": \"C\", \"step\": 30 } ], \"thresholds\": [], \"timeFrom\": null, \"timeShift\": null, \"title\": \"Memory usage\", \"tooltip\": { \"msResolution\": false, \"shared\": true, \"sort\": 2, \"value_type\": \"cumulative\" }, \"type\": \"graph\", \"xaxis\": { \"buckets\": null, \"mode\": \"time\", \"name\": null, \"show\": true, \"values\": [] }, \"yaxes\": [ { \"format\": \"bytes\", \"label\": null, \"logBase\": 1, \"max\": null, \"min\": null, \"show\": true }, { \"format\": \"bytes\", \"label\": null, \"logBase\": 1, \"max\": null, \"min\": null, \"show\": true } ], \"yaxis\": { \"align\": false, \"alignLevel\": null } }, { \"aliasColors\": {}, \"bars\": false, \"dashLength\": 10, \"dashes\": false, \"datasource\": \"${DS_PROMETHEUS}\", \"fill\": 1, \"gridPos\": { \"h\": 9, \"w\": 24, \"x\": 0, \"y\": 32 }, \"id\": 12, \"legend\": { \"alignAsTable\": true, \"avg\": false, \"current\": true, \"max\": false, \"min\": false, \"rightSide\": true, \"show\": true, \"sort\": \"current\", \"sortDesc\": true, \"total\": false, \"values\": true }, \"lines\": true, \"linewidth\": 1, \"links\": [], \"nullPointMode\": \"null\", \"percentage\": false, \"pointradius\": 5, \"points\": false, \"renderer\": \"flot\", \"seriesOverrides\": [], \"spaceLength\": 10, \"stack\": false, \"steppedLine\": false, \"targets\": [ { \"expr\": \"100 * (kubelet_volume_stats_used_bytes{kubernetes_io_hostname=~\\\"^$Node$\\\", persistentvolumeclaim=~\\\".*$Deployment$Statefulset$Daemonset.*$\\\"} / kubelet_volume_stats_capacity_bytes{kubernetes_io_hostname=~\\\"^$Node$\\\", persistentvolumeclaim=~\\\".*$Deployment$Statefulset$Daemonset.*$\\\"})\", \"format\": \"time_series\", \"intervalFactor\": 2, \"legendFormat\": \"{{ persistentvolumeclaim }} | {{ kubernetes_io_hostname }}\", \"refId\": \"A\", \"step\": 120 } ], \"thresholds\": [], \"timeFrom\": null, \"timeShift\": null, \"title\": \"Disk Usage\", \"tooltip\": { \"shared\": true, \"sort\": 2, \"value_type\": \"individual\" }, \"type\": \"graph\", \"xaxis\": { \"buckets\": null, \"mode\": \"time\", \"name\": null, \"show\": true, \"values\": [] }, \"yaxes\": [ { \"format\": \"percent\", \"label\": null, \"logBase\": 1, \"max\": null, \"min\": null, \"show\": true }, { \"format\": \"short\", \"label\": null, \"logBase\": 1, \"max\": null, \"min\": null, \"show\": false } ], \"yaxis\": { \"align\": false, \"alignLevel\": null } }, { \"aliasColors\": {}, \"bars\": false, \"dashLength\": 10, \"dashes\": false, \"datasource\": \"${DS_PROMETHEUS}\", \"decimals\": 2, \"editable\": true, \"error\": false, \"fill\": 1, \"grid\": {}, \"gridPos\": { \"h\": 13, \"w\": 24, \"x\": 0, \"y\": 41 }, \"id\": 13, \"legend\": { \"alignAsTable\": true, \"avg\": true, \"current\": true, \"max\": true, \"min\": false, \"rightSide\": true, \"show\": true, \"sideWidth\": null, \"sort\": \"current\", \"sortDesc\": true, \"total\": false, \"values\": true }, \"lines\": true, \"linewidth\": 2, \"links\": [], \"nullPointMode\": \"connected\", \"percentage\": false, \"pointradius\": 5, \"points\": false, \"renderer\": \"flot\", \"seriesOverrides\": [], \"spaceLength\": 10, \"stack\": false, \"steppedLine\": false, \"targets\": [ { \"expr\": \"sum (rate (container_network_receive_bytes_total{id!=\\\"/\\\",pod_name=~\\\"^$Deployment$Statefulset$Daemonset.*$\\\",kubernetes_io_hostname=~\\\"^$Node$\\\"}[1m])) by (pod_name, kubernetes_io_hostname)\", \"format\": \"time_series\", \"interval\": \"10s\", \"intervalFactor\": 1, \"legendFormat\": \"-> {{ kubernetes_io_hostname }} | {{ pod_name }}\", \"metric\": \"network\", \"refId\": \"A\", \"step\": 60 }, { \"expr\": \"- sum( rate (container_network_transmit_bytes_total{id!=\\\"/\\\",pod_name=~\\\"^$Deployment$Statefulset$Daemonset.*$\\\",kubernetes_io_hostname=~\\\"^$Node$\\\"}[1m])) by (pod_name, kubernetes_io_hostname)\", \"format\": \"time_series\", \"interval\": \"10s\", \"intervalFactor\": 1, \"legendFormat\": \"<- {{ kubernetes_io_hostname }} | {{ pod_name }}\", \"metric\": \"network\", \"refId\": \"B\", \"step\": 60 } ], \"thresholds\": [], \"timeFrom\": null, \"timeShift\": null, \"title\": \"All processes network I/O\", \"tooltip\": { \"msResolution\": false, \"shared\": true, \"sort\": 2, \"value_type\": \"cumulative\" }, \"type\": \"graph\", \"xaxis\": { \"buckets\": null, \"mode\": \"time\", \"name\": null, \"show\": true, \"values\": [] }, \"yaxes\": [ { \"format\": \"Bps\", \"label\": null, \"logBase\": 1, \"max\": null, \"min\": null, \"show\": true }, { \"format\": \"short\", \"label\": null, \"logBase\": 1, \"max\": null, \"min\": null, \"show\": false } ], \"yaxis\": { \"align\": false, \"alignLevel\": null } } ], \"refresh\": \"30s\", \"schemaVersion\": 16, \"style\": \"dark\", \"tags\": [ \"kubernetes\", \"deployment\" ], \"templating\": { \"list\": [ { \"allValue\": \"()\", \"current\": {}, \"datasource\": \"${DS_PROMETHEUS}\", \"hide\": 0, \"includeAll\": true, \"label\": null, \"multi\": false, \"name\": \"Deployment\", \"options\": [], \"query\": \"label_values(deployment)\", \"refresh\": 1, \"regex\": \"\", \"sort\": 0, \"tagValuesQuery\": \"\", \"tags\": [], \"tagsQuery\": \"\", \"type\": \"query\", \"useTags\": false }, { \"allValue\": \"()\", \"current\": {}, \"datasource\": \"${DS_PROMETHEUS}\", \"hide\": 0, \"includeAll\": true, \"label\": null, \"multi\": false, \"name\": \"Statefulset\", \"options\": [], \"query\": \"label_values(statefulset)\", \"refresh\": 1, \"regex\": \"\", \"sort\": 0, \"tagValuesQuery\": \"\", \"tags\": [], \"tagsQuery\": \"\", \"type\": \"query\", \"useTags\": false }, { \"allValue\": \"()\", \"current\": {}, \"datasource\": \"${DS_PROMETHEUS}\", \"hide\": 0, \"includeAll\": true, \"label\": null, \"multi\": false, \"name\": \"Daemonset\", \"options\": [], \"query\": \"label_values(daemonset)\", \"refresh\": 1, \"regex\": \"\", \"sort\": 0, \"tagValuesQuery\": \"\", \"tags\": [], \"tagsQuery\": \"\", \"type\": \"query\", \"useTags\": false }, { \"allValue\": \".*\", \"current\": {}, \"datasource\": \"${DS_PROMETHEUS}\", \"hide\": 0, \"includeAll\": true, \"label\": null, \"multi\": false, \"name\": \"Node\", \"options\": [], \"query\": \"label_values(kubernetes_io_hostname)\", \"refresh\": 1, \"regex\": \"\", \"sort\": 0, \"tagValuesQuery\": \"\", \"tags\": [], \"tagsQuery\": \"\", \"type\": \"query\", \"useTags\": false } ] }, \"time\": { \"from\": \"now-3h\", \"to\": \"now\" }, \"timepicker\": { \"refresh_intervals\": [ \"5s\", \"10s\", \"30s\", \"1m\", \"5m\", \"15m\", \"30m\", \"1h\", \"2h\", \"1d\" ], \"time_options\": [ \"5m\", \"15m\", \"1h\", \"6h\", \"12h\", \"24h\", \"2d\", \"7d\", \"30d\" ] }, \"timezone\": \"browser\", \"title\": \"1. Kubernetes Deployment Statefulset Daemonset metrics\", \"uid\": \"oWe9aYxmk\", \"version\": 7} 导入到 grafana 后,即可看到效果咯: 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"K8s","slug":"K8s","permalink":"http://damon008.github.io/tags/K8s/"}]},{"title":"如何利用k8s拉取私有仓库镜像","date":"2020-08-19T09:15:58.000Z","path":"2020/08/19/k8s-image/","text":"现象最近实战时,发现一个很奇怪的问题,在通过 k8s 创建 pod,拉取镜像时,总是显示如下信息: 1Error syncing pod, skipping: failed to \"StartContainer\" for \"POD\" with ImagePullBackOff: \"Back-off pulling image ...\" 该现象出现的原因可能是网络问题、docker 环境问题等。但如果访问的是一个公开的镜像仓库,在 pull image 的时候,不应该会提示:ImagePullBackOff,但如果访问的是私有仓库,那就有可能出现如下的错误: 这个错误出现的原因,刚才说了,有可能的网络问题,也有可能是 docker 问题,但有时候,这些不能解决的情况下,可以采用下面三种方式来解决。 方式一 第一种方式,我们可以使用文件生成 secret,然后通过 k8s 中的 imagePullSecrets 来解决拉取镜像时的验证问题。具体方式如下: 修改 /etc/docker/daemon.json在 k8s 集群节点上,修改 docker 的 daemon.json 配置文件: 1234{\"registry-mirrors\": [ \"https://registry.docker-cn.com\"],\"insecure-registries\":[\"私有仓库服务地址\"]} 在里面加上自己私有的仓库服务地址,然后重启 docker 服务,使其生效。 生成 ~/.docker/config.json 文件1docker login 私有服务地址 在命令行输入上面的命令,回车后,会提示输入用户名和密码。输入正确信息后,这会生成一个 /root/.docker/config.json 文件。同时会提示: 1Login Succeeded 生成 Secret 串根据上面生成的 ~/.docker/config.json 文件,我们可以生成一个密文秘钥: 1base64 -w 0 ~/.docker/config.json 执行上面的命令后,会生成一个长串,即为我们所要的 Secret 串。 我们会在 source 下看见一个新的文件夹,_drafts,这个里面会装我们所有的草稿文件。 创建 Secret通过 k8s 我们可以生成一个 Secret 资源: 12345678apiVersion: v1kind: Secretmetadata: name: docker_reg_secret namespace: defaultdata: .dockerconfigjson: ewoJImF1dGhjNWdlpHVnVaenB5Wld4aFFFeFdUa2xCVGtBeU1ERTMiCgkJfASEkidXJlZy5rOHMueXVud2VpLnJlbGEubWUiOiB7CgkJCSJhdXRoIjogIloyRnZaM1Z2WkdWdVSrsaaehoUUV4V1RrbEJUa0F5TURFMyIKCQl9Cgl9LAoJIkh0dHBIZWFkZXJzIjogewoJSetcaFTssZW50IjogIkRvY2tlci1DbGllbnQvMTguMDYuMS1jZSAobGludXgpIgoJfQp9type: kubernetes.io/dockerconfigjson 执行这个资源的配置: 1kubectl create -f secret.yml 在服务配置加上依赖最后,可以在 我们的服务 yml 文件中加上拉取镜像时的依赖 secret,部分代码如下: 12imagePullSecrets:- name: docker_reg_secret 方式二 第二种方式,我们可以直接使用 docker 的用户信息来生成 secret: 1kubectl create secret docker-registry docker_reg_secret --docker-server=XXX --docker-username=XXX --docker-password=XXX 参数含义: docker_reg_secret: 指定密钥的键名称, 自定义 docker-server: 指定 docker 仓库地址 docker-username: 指定 docker 仓库账号 docker-password: 指定 docker 仓库密码 创建完 Secret 资源后,其他的如方式一,这就简单了。 方式三 第三种方式所使用的是最简单的办法,即我们利用 k8s 的拉取镜像的策略来处理,主要有如下三种: Always:每次创建时都会拉取镜像 IfNotPresent:宿主机不存在时拉取镜像 Never: 从不主动拉取镜像 使用 IfNotPresent、Never 策略来处理。 以上三种方式,我比较推荐第二种,最中意第二种,因为假如密码修改了,就更新一下 secret 就好了,k8s node 不需要改动。而第一种需要改动,第三种会导致镜像丢失,毕竟只有本地存在。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"K8s","slug":"K8s","permalink":"http://damon008.github.io/tags/K8s/"}]},{"title":"个站建立基础教程","date":"2020-08-16T10:12:17.000Z","path":"2020/08/16/new-web/","text":"什么是 HexoHexo 是一个静态网站生成器,基于 Hexo 框架,可以生成自己想要的网站风格,以及网站内容,样式自己可以定义。 实战 环境准备12345系统: win7 +nodejs:http://nodejs.cn/download/git-client:https://git-scm.com/download/ 安装 Hexo准备好以上环境后,就可以准备安装 Hexo 基本框架: 1234567891011121314#安装基本框架npm install -g hexo#初始化hexo框架hexo init#安装所需要的组件npm install#编译生成静态页面hexo g#启动服务hexo s 这是一个基本的 Hexo 原型,当然,Hexo 有许多 themes,官方地址:https://hexo.io/themes/index.html,本文实战用的是Ayer。可以先从github官网拉取相关themes的基础源码。 拉取源码后,在其根目录下,进行一些基本的安装组件操作: 组件12345678910111213141516171819202122npm install [email protected] --savenpm install [email protected] --savenpm install hexo-renderer-stylus --save#用于搜索npm install hexo-generator-searchdb --save#用于生成RSS订阅npm install hexo-generator-feed --savenpm uninstall hexo-generator-index --save#用于文章置顶npm install hexo-generator-index-pin-top --save#用于文章加密,具体参考 https://github.com/MikeCoder/hexo-blog-encrypt/blob/master/ReadMe.zh.mdnpm install --save hexo-blog-encrypt#音乐播放器参考:https://github.com/MoePlayer/hexo-tag-aplayer/blob/master/docs/README-zh_cn.md 新建草稿文章1hexo new draft b 我们会在 source 下看见一个新的文件夹,_drafts,这个里面会装我们所有的草稿文件。 预览草稿12hexo server --draft 发布草稿1hexo publish b 新建正式文章1hexo new a 在 hexo 目录下的 source/_post 下生成 a.md 打开 a.md,可以编辑文章 生成页面文件12345hexo generateorhexo g 生成页面1hexo new page about 这样直接在 source 下创建 about 目录,下面也会生成一个 index.md 启动服务1hexo server 以上关于 Hexo 的基本命令以及对应的功能操作介绍完了。 我们来看看我的网站吧:交个朋友之猿天地 | 微服务 | 容器化 | 自动化。 主页展示的是个人文章,这些对于 hexo 来说就是一个个页面: 在主页可以看到左侧的栏目,这些就是 hexo 的页面,比如:_关于我_: 由于上面我们还加入了搜索插件,所以,我们可以进行全文搜索: 当然,还有一些订阅模式,等等功能。 hexo 不管是页面也好,还是文章也好,都是通过 md 格式文件来生成静态页面的,所以看起来很简单。 其次,比较重要的是有一个文件中,可以配置各种开关或格式控制: 这个里面可以根据官网配置自己想要的功能,包括打赏: 到目前为止,基于 hexo 生成静态网站的主体就到此结束啦,欢迎大家关注个站哟:交个朋友之猿天地 | 微服务 | 容器化 | 自动化。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"建站","slug":"建站","permalink":"http://damon008.github.io/tags/%E5%BB%BA%E7%AB%99/"}]},{"title":"如何保证NFS文件锁的一致性","date":"2020-08-06T03:18:42.000Z","path":"2020/08/06/nfs-01/","text":"简介 在存储系统中, NFS(Network File System,即网络文件系统)是一个重要的概念,已成为兼容POSIX语义的分布式文件系统的基础。它允许在多个主机之间共享公共文件系统,并提供数据共享的优势,从而最小化所需的存储空间。本文将通过分析NFS文件锁状态视图一致性的原理,帮助大家理解NFS的一致性设计思路。 文件锁 文件锁是文件系统的最基本特性之一,应用程序借助文件锁可以控制其他应用对文件的并发访问。NFS作为类UNIX系统的标准网络文件系统,在发展过程中逐步地原生地支持了文件锁(从NFSv4开始)。NFS从上个世界80年代诞生至今,共发布了3个版本:NFSv2、NFSv3、NFSv4。 NFSv4最大的变化是有“状态”了。某些操作需要服务端维持相关状态,如文件锁,例如客户端申请了文件锁,服务端就需要维护该文件锁的状态,否则和其他客户端冲突的访问就无法检测。如果是NFSv3就需要NLM协助才能实现文件锁功能,但是有的时候两者配合不够协调就会容易出错。而NFSv4设计成了一种有状态的协议,自身就可以实现文件锁功能,也就不需要NLM协议了。 应用接口 应用程序可以通过 fcntl() 或 flock() 系统调用管理NFS文件锁,下面是NAS使用NFSv4挂载时获取文件锁的调用过程: 从上图调用栈容易看出,NFS文件锁实现逻辑基本复用了VFS层设计和数据结构,在通过RPC从Server成功获取文件锁后,调用 locks_lock_inode_wait() 函数将获得的文件锁交给VFS层管理,关于VFS层文件锁设计的相关资料比较多,在此就不再赘述了。 EOS原理 文件锁是典型的非幂等操作,文件锁操作的重试和Failover会导致文件锁状态视图在客户端和服务端间的不一致。NFSv4借助SeqId机制设计了最多执行一次的机制,具体方法如下: 针对每个open/lock状态,Client和Server同时独立维护seqid,Client在发起会引起状态变化的操作时(open/close/lock/unlock/release_lockowner)会将seqid加1,并作为参数发送给Server,假定Client发送的seqid为R,Server维护的seqid为L,则: 若R == L +1,表示合法请求,正常处理之。若R == L,表示重试请求,Server将缓存的reply返回即可。其他情况均为非法请求,决绝访问。根据上述规则,Server可判断操作是否为正常、重试或非法请求。 该方法能够保证每个文件锁操作在服务端最多执行一次,解决了RPC重试带来的重复执行的问题,但是仅靠这一点是不够的。比如LOCK操作发送后调用线程被信号中断,此后服务端又成功接受并执行了该LOCK操作,这样服务端就记录了客户端持有了锁,但客户端中却因为中断而没有维护这把锁,于是就造成了客户端和服务端间的锁状态视图不一致。因此,客户端还需要配合处理异常场景,最终才能够保证文件锁视图一致性。 异常处理由上一节的分析可知,客户端需要配合处理异常场景才能够保证文件视图一致性,那么客户端设计者主要做了哪些配合的设计呢?目前客户端主要从SunRPC和NFS协议实现两个维度相互配合解决该问题,下面分别介绍这两个维度的设计如何保证文件锁状态视图一致性。 SunRPC设计 SunRPC是Sun公司专门为远程过程调用设计的网络通讯协议,这里从保障文件锁视图一致性的维度来了解一下SunRPC实现层面的设计理念: (1)客户端使用int32_t类型的xid标识上层使用者发起的每个远程过程调用过程,每个远程过程调用的多次RPC重试使用相同的xid标识,这样就保障了多次RPC重试中任何一个返回都可以告知上层远程过程调用已经成功,保证了服务端执行远程过程调用执行耗时较长时也能拿到结果,这一点和传统的netty/mina/brpc等都需要每个RPC都要有独立的xid/packetid不同。 (2)服务端设计了DRC(duplicate request cache)缓存最近执行的RPC结果,接收到RPC时会首先通过xid检索DRC缓存,若命中则表明RPC为重试操作,直接返回缓存的结果即可,这在一定程度上规避了RPC重试带来的重复执行的问题。为了避免xid复用导致DRC缓存返回非预期的结果,开发者通过下述设计进一步有效地减少复用引起错误的概率: 客户端建立新链接时初始xid采用随机值。服务端DRC会额外记录请求的校验信息,缓存命中时会同时校验这些信息。 (3)客户端允许在获得服务端响应前无限重试,保证调用者能够获得服务端确定性的执行结果,当然这样的策略会导致无响应时调用者会一直hang。 (4)NFS允许用户在挂载时通过soft/hard参数指定SunRPC的重试策略,其中soft模式禁止超时后重试,hard模式则持续重试。当用户使用soft模式挂载时NFS实现不保证客户端和服务端状态视图的一致性,在遇到远程过程调用返回超时要求应用程序配合状态的清理和恢复,比如关闭访问出错的文件等,然而实践中很少有应用程序会配合,所以一般情况下NAS用户都使用hard模式挂载。 总之,SunRPC要解决的核心问题之一是,远程过程调用执行时间是不可控的,协议设计者为此定制化设计,尽量避免非幂等操作RPC重试带来的副作用。 信号中断 应用程序等待远程过程调用结果时允许被信号中断。当发生信号中断时,由于没有得到远程过程调用的执行结果,所以客户端和服务端的状态很可能就不一致了,比如加锁操作在服务端已经成功执行,但客户端并不知道这个情况。这就要求客户端做额外的工作将状态和服务端恢复一致。下面简要分析获取文件锁被信号中断后的处理,来说明NFS协议实现层面的一致性设计。 通过获取NFSv4文件锁的过程可知,NFSv4获取文件锁最终会调用 _nfs4_do_setlk() 函数发起RPC操作,最终调用 nfs4_wait_for_completion_rpc_task() 等待,下面是相关代码: 12345678910111213141516static int _nfs4_do_setlk(struct nfs4_state *state, int cmd, struct file_lock *fl, int recovery_type){ ...... task = rpc_run_task(&task_setup_data); if (IS_ERR(task)) return PTR_ERR(task); ret = nfs4_wait_for_completion_rpc_task(task); if (ret == 0) { ret = data->rpc_status; if (ret) nfs4_handle_setlk_error(data->server, data->lsp, data->arg.new_lock_owner, ret); } else data->cancelled = 1; ......} 通过分析 nfs4_wait_for_completion_rpc_task() 实现可知,当ret < 0时,表明获取锁过程被信号中断,并使用 struct nfs4_lockdata 的 cancelled 成员记录。继续查看rpc_task完成后释放时的回调函数 nfs4_lock_release(): 从上面红色框中的代码可知,nfs4_lock_release() 检测到存在信号中断时会调用 nfs4_do_unlck()函数尝试将可能成功获得文件锁释放掉,注意此时没有调用 nfs_free_seqid() 函数将持有的nfs_seqid释放掉,这是为了: 保证订正状态过程中不会有用户新发起的并发加锁或者释放锁操作,简化实现。保证hard模式下UNLOCK操作只会在LOCK操作返回后才会发送,保障已经获得锁能够被释放掉。客户端通过上面的方法能够有效地保证信号中断后客户端和服务端锁状态的最终一致性,但也是在损失一部分可用性为代价的。 总结 文件锁是文件系统原生支持的基础特性,NAS作为共享的文件系统要面临客户端和服务端锁状态视图一致性的问题,NFSv4.0在一定程度上解决了这个问题,当然,技术前进的脚步不会停止,NFS的更新迭代也就不会停止,未来的NFS将会有更多的期待。 最后 我们相信技术的力量,更相信拥有技术力量的人。我们期待存储的未来,更期待与你一起创造未来。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注 特别声明 原文作者:茶什i 本文原链:https://developer.aliyun.com/article/769594?spm=a2c6h.12873581.0.dArticle769594.37366446qrd1Wv&groupCode=alitech 本文转载如有侵权,请联系站长删除,谢谢","tags":[{"name":"后端","slug":"后端","permalink":"http://damon008.github.io/tags/%E5%90%8E%E7%AB%AF/"}]},{"title":"电视剧 | 电影 | 影片 | 无广告","date":"2020-08-05T08:55:58.000Z","path":"2020/08/05/film-01/","text":"NO 视频 片库 茶杯狐 蓝光高清网","tags":[{"name":"多媒体","slug":"多媒体","permalink":"http://damon008.github.io/tags/%E5%A4%9A%E5%AA%92%E4%BD%93/"}]},{"title":"Kubernetes 经典命令","date":"2020-07-27T09:41:37.000Z","path":"2020/07/27/k8s-02/","text":"最近大家想了解 Kubernetes 常见命令,今天它来了。 如果想玩玩单机版、集群版 k8s,可参考:k8s部署手册,快速助力部署 k8s,还没毕业的都可以部署哟! k8s 常用命令: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859netstat -nlpt | grep 关键字: 查询相关的网络问题systemctl status -l kubelet: 查看kubelet状态systemctl restart kube-apiserver: 重启apiserverkubelet --version: 查看k8s版本history |grep 关键字: 查看相关操作历史kubectl cluster-info: 查看集群信息 or kubectl cluster-info dumpkubectl -n kube-system get sa: 查看所有账号kubectl get ep: 获取所有endpoints信息kubectl get svc: 获取服务 -n 空间名称,指定命名空间kubectl get pods --all-namespaces -o wide: 获取所有的podskubectl create -f *.yaml: 使用yaml文件创建pod,这个不可重复执行kubectl apply -f *.yaml: 可重复执行kubectl delete -f *.yaml: 使用yaml文件删除podkubectl logs POD_NAME -n 空间名称: 显示指定命名空间的pod的日志kubectl get nodes: 获取集群所有节点信息kubectl delete node ip: 删除节点kubectl describe node ip: 显示节点信息kubectl describe pod podName: 显示pod信息kubectl describe ep kuberneteskubectl describe svc kuberneteskubectl get svc kuberneteskubectl delete pod --all: 删除所有podkubectl exec -it podname bash or sh: 进入某个pod容器kubectl logs podname: 查看某个pod日志kubectl logs -f podname: 实时查看某个pod日志kubectl logs -f --tail=100 podname: 实时查看某个pod最新100条日志kubectl log podname -c containername: 若 pod 只有一个容器,可以不加 -ckubectl scale --replicas=2 deployment edge-cas-deployment: 以deployment形式启动2个podkubectl explain pod: 查看pod的注释kubectl explain pod.apiVersion 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"K8s","slug":"K8s","permalink":"http://damon008.github.io/tags/K8s/"}]},{"title":"Docker常用命令,你都会了吗","date":"2020-07-27T09:35:10.000Z","path":"2020/07/27/docker-01/","text":"应大家要求,今天整理下 Docker 常见的一些命令。 关于 docker 的安装,在 k8s部署手册 一文中,你可以快速安装docker的各种版本。 常见命令: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120docker images: 查看镜像,后可跟 \"| grep 内容\",可根据内容进行筛选。如:docker images | grep nginxdocker images [OPTIONS] [REPOSITORY[:TAG]]OPTIONS说明:-a: 列出本地所有的镜像--digests: 显示镜像的摘要信息-f: 显示满足条件的镜像--format: 指定返回值的模板文件--no-trunc: 显示完整的镜像信息-q: 只显示镜像IDdocker run: 创建一个新的容器并运行一个命令docker run [OPTIONS] IMAGE [COMMAND] [ARG...]OPTIONS说明:-d: 后台运行容器,并返回容器ID-i: 以交互模式运行容器,通常与 -t 同时使用-p: 指定端口映射,格式为:主机(宿主)端口:容器端口-t: 为容器重新分配一个伪输入终端,通常与 -i 同时使用--name \"nginx\": 为容器指定一个名称-h \"localhost\": 指定容器的hostname-e spring.profiles.active=\"dev\": 设置环境变量--env-file=[]: 从指定文件读环境变量-m :设置容器使用内存最大值--volume /home/data:/etc/data : 绑定一个卷and so on如:docker run -d -t -p 80:80 -v /home/data:/usr/data --name nginx nginx:latestdocker create: 创建一个新的容器但不启动它docker stop: 停止一个运行的容器docker stop containerNamedocker restart: 重启一个容器docker restart containerNamedocker start: 启动一个被停止的容器docker start containerNamedocker ps [OPTIONS]: 列出容器OPTIONS说明:-a: 显示所有的容器,包括未运行的-f: 根据条件过滤显示的内容--format: 指定返回值的模板文件-l: 显示最近创建的容器-n: 列出最近创建的n个容器--no-trunc: 不截断输出-q: 静默模式,只显示容器编号-s: 显示总的文件大小docker ps -a: 查看所有容器docker ps: 查看正在运行的容器docker exec: 进入一个运行中的容器执行命令如:docker exec -it 容器id sh or bash or /bin/bash表示在容器中开启一个交互模式的终端docker rm: 删除一个容器,可加-f 表示强制 -v:并删除挂载卷删除所有停止的容器:docker rm $(docker ps -a -q)docker rmi: 删除一个镜像,可加-f 表示强制docker inspect : 获取容器/镜像的元数据如:docker inspect [OPTIONS] NAME|ID [NAME|ID...]OPTIONS说明:-f: 指定返回值的模板文件-s: 显示总文件大小-type: 为指定类型返回json数据获取正在运行的容器 nginx 的 IP:docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' nginxdocker kill: 杀死一个运行中的容器如: docker kill -s killyou nginxdocker logs: 获取容器的日志如:docker logs -f -t 容器id or docker logs -f -t --tail=100 容器iddocker build: 命令用于使用 Dockerfile 创建镜像docker build [OPTIONS] PATH | URL | -OPTIONS说明:-f: 指定要使用的Dockerfile路径-m: 设置内存最大值--memory-swap: 设置Swap的最大值为内存+swap,\"-1\"表示不限swap--no-cache: 创建镜像的过程不使用缓存--pull: 尝试去更新镜像的新版本-q: 安静模式,成功后只输出镜像 ID--rm: 设置镜像成功后删除中间容器--shm-size: 设置/dev/shm的大小,默认值是64M--tag: 镜像的名字及标签,通常 name:tag 或者 name 格式;可以在一次构建中为一个镜像设置多个标签--network: 默认 default。在构建期间设置RUN指令的网络模式docker build -t 镜像标签名 .: docker build -t nginx:latest .docker build -f /path/to/a/Dockerfile .docker tag: 标记本地镜像,将其归入某一仓库docker tag nginx nginx:olddocker save: 将指定镜像保存成 tar 归档文件docker save -o nginx.tar nginx:latestdocker load: 导入使用 docker save 命令导出的镜像docker load -i tar文件名docker info: 查看docker环境信息docker version: 查看docker版本信息docker login: 登录一个Docker镜像仓库docker login -u 用户名 -p 密码docker logout: 退出登录docker pull: 拉取或者更新指定镜像 -a 拉取所有的tag的镜像docker push: 将本地的镜像上传到镜像仓库 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"Docker","slug":"Docker","permalink":"http://damon008.github.io/tags/Docker/"}]},{"title":"消息中间件那点事儿","date":"2020-07-27T02:26:55.000Z","path":"2020/07/27/mq-01/","text":"背景系统的稳定性一直以来是所有企业研发人员所追求的,当出现系统问题时,这时候可以通过日志、系统监控、性能指标等来进行排查,但系统的复杂性、分布式、高并发等导致了很多信息的堆积,这个时候,可能对于工作人员来说,是一件非常头痛的事情。消息中间件,英文简称 MQ,这个名词的出现,可以解决很多事情:比如:复杂的业务中通过 MQ 来减少频繁的业务交互。高并发下,不必及时处理的事务都可以交给 MQ。交易过程中,更是存在很多的事务处理,如果长时间保持长链接以及锁的状态,很有可能造成锁表、锁库,形成死锁。这时候也需要 MQ 来进行消息的缓冲、异步进行,保证系统的稳定、持续的进行下去。不会因为死锁等长时间的循环而导致 cpu、内存的增耗,从而避免出现服务挂掉、宕机等问题。 形形色色的 MQ事实上,消息中间件的种类越来越多:RabbitMQ、RocketMQ、Kafka 等等。下面给出了一张表展示各种 MQ 特点: 说到这,大家可能要说,还有一种中间件:Redis,的确,Redis 也作为一种中间件,一般用来作为缓存中间件。缓存一些信息,以便数据信息共享、也可以利用其来实现分布式锁,例如实现秒杀、抢单等功能。还会被用作一些订单信息的缓存,防止大量的订单信息被积压而导致服务器的负载很高。总之,Redis 常被用来作为一种缓冲剂使用。 除了上面说的,消息中间件还可以用来抢红包,交易系统的账单记录、流程推送、通知等等。Redis 作为缓存处理器,它的使用,大大的提升了应用的性能与效率,特别是在查询数据的层面上,大大降低了查询数据库的频率。但这也带来了一些问题,其中比较典型的,比如:缓存穿透、缓存击穿、缓存雪崩。 什么是缓存穿透呢? 我们先来看看缓存的查询流程:前端发来请求查询数据时,后端首先会在缓存 Redis 中查询,如果查询到数据,直接返回给前端,流程结束;如果在缓存中未查到数据,则前往数据库查找,此时查询数据后返回给前端,同时会塞进缓存中。还有一种可能就是:查询数据库未查到数据时,会直接返回 NULL。 这种情况下,如果用户不停滴发起请求时,恶意提供数据库中不存在的信息,则在数据库中查到的数据永远都是 NULL。这种数据是不会被塞进缓存的,这种的数据永远会被从数据库中访问,即为恶意攻击式,则很有可能对数据库造成极大的压力,搞哭数据库。这个过程被称为:缓存穿透。缓存永远被直接穿透而直接访问数据库。 解决方案 目前对于缓存穿透,比较典型的解决方案是:当在数据库查询未找到时,将 NULL 返给前端,同时,会将 NULL 塞入缓存,并对对应的 Key 设置一定的过期时间。 这种处理方式在电商的话,用到的较多。 什么是缓存击穿呢? 缓存击穿,是指缓存中某个 Key 在不停的、频繁的被请求,当这个 Key 在某个时刻失效时,持续的高并发请求就会击穿缓存,直接请求数据库,导致数据库的压力在那一时刻猛增。就像水滴石穿。 解决方案既然这种 key 会被不停的访问、请求,那么可以将其有效期设为一万年,这样,不停的高并发请求,就永不会落在数据库层。 什么是缓存雪崩呢? 缓存雪崩,是指在某个时刻,缓存的 key 集体发生失效,这样导致大量的查询请求落在了数据库层,导致数据库负载过高,甚至会压垮数据库。 解决方案雪崩的现象,主要在于大量的 key 在同一时刻处于失效状态,所以为了避免这种情况:一般会为 key 设置不同的、随机的失效时间,错开缓存中 key 的失效时间点,从而最终减少数据库压力。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 12https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"后端","slug":"后端","permalink":"http://damon008.github.io/tags/%E5%90%8E%E7%AB%AF/"}]},{"title":"浅谈微服务安全架构设计","date":"2020-07-24T05:57:33.000Z","path":"2020/07/24/micro-service01/","text":"1、 回顾微服务设计理念 在 浅入微服务架构 一文中,我们了解到什么是微服务,微服务的划分依据,其实,说到底,微服务的设计,有其独到的好处:使得各个模块之间解耦合,让每一个模块有自己独立的灵魂,其他服务即使出现任何问题,自己不会受到任何的影响。这是微服务的核心宗旨。那么今天要讲的微服务安全性问题,其实也是反映微服务的一个核心:高内聚。所谓高内聚,简单的理解就是,对外暴露的最小限度,降低其依赖关系,大部分都作为一个黑盒子封装起来,不直接对外,这样,即使内部发生变更、翻云覆雨,对外的接口没发生改变,这才是好的微服务设计理念,做到完美的对外兼容,一个好的架构设计,首先,这一点可能需要 get 到位,不知道大家咋认为呢?所以今天说的微服务安全性,就跟这个高内聚有一点点相关了。或者说,体现了微服务设计的核心理念。 2、微服务下的各种安全性保证 2.1 常见的几种安全性措施在微服务中,我们常见的,有如下几种安全性设计的举措:网关设计、服务端口的对外暴露的限度、token 鉴权、OAuth2 的统一认证、微信中的 openId 设计等。这些都是在为服务的安全性作考虑的一些举措。 2.2 OAuth2 的概念何为 OAuth2 呢?我们先了解 OAuth,Oauth 是一个开放标准,假设有这样一种场景:一个 QQ 应用,希望让一个第三方的(慕课网)应用,能够得到关于自身的一些信息(唯一用户标识,比如说 QQ 号,用户个人信息、一些基础资料,昵称和头像等)。但是在获得这些资料的同时,却又不能提供用户名和密码之类的信息。如下图: 而 OAuth 就是实现上述目标的一种规范。OAuth2 是 OAuth 协议的延续版本,但不兼容 OAuth1.0,即完全废弃了 OAuth1.0。 OAuth2.0 有这么几个术语:客户凭证、令牌、作用域。 客户凭证:客户的 clientId 和密码用于认证客户。 令牌:授权服务器在接收到客户请求后颁发的令牌。 作用域:客户请求访问令牌时,由资源拥有者额外指定的细分权限。 2.3 OAuth2 的原理在 OAuth2 的授权机制中有 4 个核心对象: Resource Owner:资源拥有者,即用户。 Client:第三方接入平台、应用,请求者。 Resource Server:资源服务器,存储用户信息、用户的资源信息等资源。 Authorization Server:授权认证服务器。 实现机制: 用户在第三方应用上点击登录,应用向认证服务器发送请求,说有用户希望进行授权操作,同时说明自己是谁、用户授权完成后的回调 url,例如:上面的截图,通过慕课网访问 QQ 获取授权。 认证服务器展示给用户自己的授权界面。 用户进行授权操作,认证服务器验证成功后,生成一个授权编码 code,并跳转到第三方的回调 url。 第三方应用拿到 code 后,连同自己在平台上的身份信息(ID 密码)发送给认证服务器,再一次进行验证请求,说明自己的身份正确,并且用户也已经授权我了,来换取访问用户资源的权限。 认证服务器对请求信息进行验证,如果没问题,就生成访问资源服务器的令牌 access_token,交给第三方应用。 第三方应用使用 access_token 向资源服务器请求资源。 资源服务器验证 access_token 成功后返回响应资源。 2.4 OAuth2 的几种授权模式OAuth2.0 有这么几个授权模式:授权码模式、简化模式、密码模式、客户端凭证模式。 授权码模式:(authorization_code)是功能最完整、流程最严密的授权模式,code 保证了 token 的安全性,即使 code 被拦截,由于没有 client_secret,也是无法通过 code 获得 token 的。 简化模式:和授权码模式类似,只不过少了获取 code 的步骤,是直接获取令牌 token 的,适用于公开的浏览器单页应用,令牌直接从授权服务器返回,不支持刷新令牌,且没有 code 安全保证,令牌容易因为被拦截窃听而泄露。 密码模式:使用用户名/密码作为授权方式从授权服务器上获取令牌,一般不支持刷新令牌。 客户端凭证模式:一般用于资源服务器是应用的一个后端模块,客户端向认证服务器验证身份来获取令牌。 2.5 实战 OAuth2 的密码模式本次结合 Spring Cloud Alibaba 组件,实现微服务的安全系统体系,本文主要讲解 OAuth2 的部分。 先来看鉴权中心,鉴权中心需要做到提供单点服务,为所有的客户端微服务的安全保驾护航。下面首先看依赖: 1234567891011121314151617181920<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId></dependency><dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <!-- 对redis支持,引入的话项目缓存就支持redis了,所以必须加上redis的相关配置,否则操作相关缓存会报异常 --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency> 如果需要使用 redis 来存储 token,则可以加入 reids 依赖,如果使用 jwt,则使用: 12345<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version></dependency> 当然,本次的项目模块引入的是比较新的 Spring Boot: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.13.RELEASE</version> <relativePath/></parent><properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <swagger.version>2.6.1</swagger.version> <xstream.version>1.4.7</xstream.version> <pageHelper.version>4.1.6</pageHelper.version> <fastjson.version>1.2.51</fastjson.version> <springcloud.version>Greenwich.SR3</springcloud.version> <mysql.version>5.1.46</mysql.version> <alibaba-cloud.version>2.1.1.RELEASE</alibaba-cloud.version> <springcloud.alibaba.version>0.9.0.RELEASE</springcloud.alibaba.version> </properties><dependencyManagement> <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${alibaba-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!-- <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${springcloud.alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${springcloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement> 剩下的,像数据库、持久化等,其他的可以根据需要添加。 配置完成后,我们需要写一个认证服务器的配置: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163package com.damon.config;import java.util.ArrayList;import java.util.List;import javax.sql.DataSource;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.context.annotation.Configuration;import org.springframework.core.env.Environment;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;import org.springframework.security.oauth2.provider.token.TokenEnhancer;import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;import org.springframework.security.oauth2.provider.token.TokenStore;import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;import com.damon.component.JwtTokenEnhancer;import com.damon.login.service.LoginService;@Configuration@EnableAuthorizationServerpublic class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private PasswordEncoder passwordEncoder; @Autowired private AuthenticationManager authenticationManager; @Autowired private LoginService loginService; @Autowired //@Qualifier(\"jwtTokenStore\") @Qualifier(\"redisTokenStore\") private TokenStore tokenStore; /*@Autowired private JwtAccessTokenConverter jwtAccessTokenConverter; @Autowired private JwtTokenEnhancer jwtTokenEnhancer;*/ @Autowired private Environment env; @Autowired private DataSource dataSource; @Autowired private WebResponseExceptionTranslator userOAuth2WebResponseExceptionTranslator; /** * redis token 方式 */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { //验证时发生的情况处理 endpoints.authenticationManager(authenticationManager) //支持 password 模式 .exceptionTranslator(userOAuth2WebResponseExceptionTranslator)//自定义异常处理类添加到认证服务器配置 .userDetailsService(loginService) .tokenStore(tokenStore); } /** * 客户端配置(给谁发令牌) * 不同客户端配置不同 * * authorizedGrantTypes 可以包括如下几种设置中的一种或多种: authorization_code:授权码类型。需要redirect_uri implicit:隐式授权类型。需要redirect_uri password:资源所有者(即用户)密码类型。 client_credentials:客户端凭据(客户端ID以及Key)类型。 refresh_token:通过以上授权获得的刷新令牌来获取新的令牌。 accessTokenValiditySeconds:token 的有效期 scopes:用来限制客户端访问的权限,在换取的 token 的时候会带上 scope 参数,只有在 scopes 定义内的,才可以正常换取 token。 * @param clients * @throws Exception * @author Damon * */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient(\"provider-service\") .secret(passwordEncoder.encode(\"provider-service-123\")) .accessTokenValiditySeconds(3600) .refreshTokenValiditySeconds(864000)//配置刷新token的有效期 .autoApprove(true) //自动授权配置 .scopes(\"all\")//配置申请的权限范围 .authorizedGrantTypes(\"password\", \"authorization_code\", \"client_credentials\", \"refresh_token\")//配置授权模式 .redirectUris(\"http://localhost:2001/login\")//授权码模式开启后必须指定 .and() .withClient(\"consumer-service\") .secret(passwordEncoder.encode(\"consumer-service-123\")) .accessTokenValiditySeconds(3600) .refreshTokenValiditySeconds(864000)//配置刷新token的有效期 .autoApprove(true) //自动授权配置 .scopes(\"all\")//配置申请的权限范围 .authorizedGrantTypes(\"password\", \"authorization_code\", \"client_credentials\", \"refresh_token\")//配置授权模式 .redirectUris(\"http://localhost:2005/login\")//授权码模式开启后必须指定 .and() .withClient(\"resource-service\") .secret(passwordEncoder.encode(\"resource-service-123\")) .accessTokenValiditySeconds(3600) .refreshTokenValiditySeconds(864000)//配置刷新token的有效期 .autoApprove(true) //自动授权配置 .scopes(\"all\")//配置申请的权限范围 .authorizedGrantTypes(\"password\", \"authorization_code\", \"client_credentials\", \"refresh_token\")//配置授权模式 .redirectUris(\"http://localhost:2006/login\")//授权码模式开启后必须指定 .and() .withClient(\"test-sentinel\") .secret(passwordEncoder.encode(\"test-sentinel-123\")) .accessTokenValiditySeconds(3600) .refreshTokenValiditySeconds(864000)//配置刷新token的有效期 .autoApprove(true) //自动授权配置 .scopes(\"all\")//配置申请的权限范围 .authorizedGrantTypes(\"password\", \"authorization_code\", \"client_credentials\", \"refresh_token\")//配置授权模式 .redirectUris(\"http://localhost:2008/login\")//授权码模式开启后必须指定 .and() .withClient(\"test-sentinel-feign\") .secret(passwordEncoder.encode(\"test-sentinel-feign-123\")) .accessTokenValiditySeconds(3600) .refreshTokenValiditySeconds(864000)//配置刷新token的有效期 .autoApprove(true) //自动授权配置 .scopes(\"all\")//配置申请的权限范围 .authorizedGrantTypes(\"password\", \"authorization_code\", \"client_credentials\", \"refresh_token\")//配置授权模式 .redirectUris(\"http://localhost:2010/login\")//授权码模式开启后必须指定 .and() .withClient(\"customer-service\") .secret(passwordEncoder.encode(\"customer-service-123\")) .accessTokenValiditySeconds(3600) .refreshTokenValiditySeconds(864000)//配置刷新token的有效期 .autoApprove(true) //自动授权配置 .scopes(\"all\") .authorizedGrantTypes(\"password\", \"authorization_code\", \"client_credentials\", \"refresh_token\")//配置授权模式 .redirectUris(\"http://localhost:2012/login\")//授权码模式开启后必须指定 ; } @Override public void configure(AuthorizationServerSecurityConfigurer security) { security.allowFormAuthenticationForClients();//是允许客户端访问 OAuth2 授权接口,否则请求 token 会返回 401 security.checkTokenAccess(\"isAuthenticated()\");//是允许已授权用户访问 checkToken 接口 security.tokenKeyAccess(\"isAuthenticated()\"); // security.tokenKeyAccess(\"permitAll()\");获取密钥需要身份认证,使用单点登录时必须配置,是允许已授权用户获取 token 接口 }} Redis 配置: 123456789101112131415161718192021package com.damon.config;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.security.oauth2.provider.token.TokenStore;import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;@Configurationpublic class RedisTokenStoreConfig { @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean public TokenStore redisTokenStore (){ //return new RedisTokenStore(redisConnectionFactory); return new MyRedisTokenStore(redisConnectionFactory); }} 后面接下来需要配置安全访问的拦截,这时候需要 SpringSecurity: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061package com.damon.config;import javax.servlet.http.HttpServletResponse;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.builders.WebSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;@Configuration@EnableWebSecuritypublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override public void configure(HttpSecurity http) throws Exception { http.csrf() .disable() .exceptionHandling() .authenticationEntryPoint(new AuthenticationEntryPointHandle()) //.authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED)) .and() .authorizeRequests() .antMatchers(\"/oauth/**\", \"/login/**\")//\"/logout/**\" .permitAll() .anyRequest() .authenticated() .and() .formLogin() .permitAll(); } /*@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); }*/ @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers(\"/css/**\", \"/js/**\", \"/plugins/**\", \"/favicon.ico\"); }} 再者,就是需要配置资源拦截: 12345678910111213141516171819202122232425262728293031package com.damon.config;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Configuration;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;@Configuration@EnableResourceServerpublic class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.csrf().disable() .exceptionHandling() .authenticationEntryPoint(new AuthenticationEntryPointHandle()) //.authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED)) .and() .requestMatchers().antMatchers(\"/api/**\") .and() .authorizeRequests() .antMatchers(\"/api/**\").authenticated() .and() .httpBasic(); }} 其中,在上面我们配置了资源拦截、权限拦截的统一处理配置: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849package com.damon.config;import java.io.IOException;import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.springframework.http.HttpStatus;import org.springframework.security.core.AuthenticationException;import org.springframework.security.web.AuthenticationEntryPoint;import com.alibaba.fastjson.JSON;import com.damon.commons.Response;/** * * 统一结果处理 * * @author Damon * */public class AuthenticationEntryPointHandle implements AuthenticationEntryPoint { /** * * @author Damon * */ @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { //response.setStatus(HttpServletResponse.SC_FORBIDDEN); //response.setStatus(HttpStatus.OK.value()); //response.setHeader(\"Access-Control-Allow-Origin\", \"*\"); //gateway已加,无需再加 //response.setHeader(\"Access-Control-Allow-Headers\", \"token\"); //解决低危漏洞点击劫持 X-Frame-Options Header未配置 response.setHeader(\"X-Frame-Options\", \"SAMEORIGIN\"); response.setCharacterEncoding(\"UTF-8\"); response.setContentType(\"application/json; charset=utf-8\"); response.getWriter() .write(JSON.toJSONString(Response.ok(response.getStatus(), -2, authException.getMessage(), null))); /*response.getWriter() .write(JSON.toJSONString(Response.ok(200, -2, \"Internal Server Error\", authException.getMessage())));*/ }} 最后,自定义异常处理类添加到认证服务器配置: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126package com.damon.config;import java.io.IOException;import org.springframework.http.HttpHeaders;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.security.access.AccessDeniedException;import org.springframework.security.core.AuthenticationException;import org.springframework.security.oauth2.common.DefaultThrowableAnalyzer;import org.springframework.security.oauth2.common.exceptions.InsufficientScopeException;import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;import org.springframework.security.web.util.ThrowableAnalyzer;import org.springframework.stereotype.Component;import org.springframework.web.HttpRequestMethodNotSupportedException;import com.damon.exception.UserOAuth2Exception;/** * * 自定义异常转换类 * @author Damon * */@Component(\"userOAuth2WebResponseExceptionTranslator\")public class UserOAuth2WebResponseExceptionTranslator implements WebResponseExceptionTranslator { private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer(); @Override public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception { Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(e); Exception ase = (OAuth2Exception)this.throwableAnalyzer.getFirstThrowableOfType(OAuth2Exception.class, causeChain); //异常链中有OAuth2Exception异常 if (ase != null) { return this.handleOAuth2Exception((OAuth2Exception)ase); } //身份验证相关异常 ase = (AuthenticationException)this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain); if (ase != null) { return this.handleOAuth2Exception(new UserOAuth2WebResponseExceptionTranslator.UnauthorizedException(e.getMessage(), e)); } //异常链中包含拒绝访问异常 ase = (AccessDeniedException)this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain); if (ase instanceof AccessDeniedException) { return this.handleOAuth2Exception(new UserOAuth2WebResponseExceptionTranslator.ForbiddenException(ase.getMessage(), ase)); } //异常链中包含Http方法请求异常 ase = (HttpRequestMethodNotSupportedException)this.throwableAnalyzer.getFirstThrowableOfType(HttpRequestMethodNotSupportedException.class, causeChain); if(ase instanceof HttpRequestMethodNotSupportedException){ return this.handleOAuth2Exception(new UserOAuth2WebResponseExceptionTranslator.MethodNotAllowed(ase.getMessage(), ase)); } return this.handleOAuth2Exception(new UserOAuth2WebResponseExceptionTranslator.ServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), e)); } private ResponseEntity<OAuth2Exception> handleOAuth2Exception(OAuth2Exception e) throws IOException { int status = e.getHttpErrorCode(); HttpHeaders headers = new HttpHeaders(); headers.set(\"Cache-Control\", \"no-store\"); headers.set(\"Pragma\", \"no-cache\"); if (status == HttpStatus.UNAUTHORIZED.value() || e instanceof InsufficientScopeException) { headers.set(\"WWW-Authenticate\", String.format(\"%s %s\", \"Bearer\", e.getSummary())); } UserOAuth2Exception exception = new UserOAuth2Exception(e.getMessage(),e); ResponseEntity<OAuth2Exception> response = new ResponseEntity(exception, headers, HttpStatus.valueOf(status)); return response; } private static class MethodNotAllowed extends OAuth2Exception { public MethodNotAllowed(String msg, Throwable t) { super(msg, t); } @Override public String getOAuth2ErrorCode() { return \"method_not_allowed\"; } @Override public int getHttpErrorCode() { return 405; } } private static class UnauthorizedException extends OAuth2Exception { public UnauthorizedException(String msg, Throwable t) { super(msg, t); } @Override public String getOAuth2ErrorCode() { return \"unauthorized\"; } @Override public int getHttpErrorCode() { return 401; } } private static class ServerErrorException extends OAuth2Exception { public ServerErrorException(String msg, Throwable t) { super(msg, t); } @Override public String getOAuth2ErrorCode() { return \"server_error\"; } @Override public int getHttpErrorCode() { return 500; } } private static class ForbiddenException extends OAuth2Exception { public ForbiddenException(String msg, Throwable t) { super(msg, t); } @Override public String getOAuth2ErrorCode() { return \"access_denied\"; } @Override public int getHttpErrorCode() { return 403; } }} 最后,我们可能需要配置一些请求客户端的配置,以及变量配置: 123456789101112131415@Configurationpublic class BeansConfig { @Resource private Environment env; @Bean public RestTemplate restTemplate() { SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); requestFactory.setReadTimeout(env.getProperty(\"client.http.request.readTimeout\", Integer.class, 15000)); requestFactory.setConnectTimeout(env.getProperty(\"client.http.request.connectTimeout\", Integer.class, 3000)); RestTemplate rt = new RestTemplate(requestFactory); return rt; }} 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263package com.damon.config;import org.springframework.beans.factory.annotation.Value;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.cloud.context.config.annotation.RefreshScope;import org.springframework.context.annotation.Configuration;import org.springframework.stereotype.Component;/** * 配置信息 * @author Damon * */@Component@RefreshScopepublic class EnvConfig { @Value(\"${jdbc.driverClassName:}\") private String jdbc_driverClassName; @Value(\"${jdbc.url:}\") private String jdbc_url; @Value(\"${jdbc.username:}\") private String jdbc_username; @Value(\"${jdbc.password:}\") private String jdbc_password; public String getJdbc_driverClassName() { return jdbc_driverClassName; } public void setJdbc_driverClassName(String jdbc_driverClassName) { this.jdbc_driverClassName = jdbc_driverClassName; } public String getJdbc_url() { return jdbc_url; } public void setJdbc_url(String jdbc_url) { this.jdbc_url = jdbc_url; } public String getJdbc_username() { return jdbc_username; } public void setJdbc_username(String jdbc_username) { this.jdbc_username = jdbc_username; } public String getJdbc_password() { return jdbc_password; } public void setJdbc_password(String jdbc_password) { this.jdbc_password = jdbc_password; }} 最后需要配置一些环境配置: 1234567891011121314151617181920212223spring: application: name: oauth-cas cloud: nacos: discovery: server-addr: 127.0.0.1:8848 config: server-addr: 127.0.0.1:8848 refreshable-dataids: actuator.properties,log.properties redis: #redis相关配置 database: 8 host: 127.0.0.1 #localhost port: 6379 password: aaa #有密码时设置 jedis: pool: max-active: 8 max-idle: 8 min-idle: 0 timeout: 10000ms 记住:上面这个启动配置需要在 bootstrap 文件中添加,否则,可能会失败,大家可以尝试下。 1234567891011121314151617181920212223242526272829server: port: 2000 undertow: uri-encoding: UTF-8 accesslog: enabled: false pattern: combined #这里我们使用了SpringBoot2.x,注意session与1.x不同 servlet: session: timeout: PT120M cookie: name: OAUTH-CAS-SESSIONID #防止Cookie冲突,冲突会导致登录验证不通过client: http: request: connectTimeout: 8000 readTimeout: 30000mybatis: mapperLocations: classpath:mapper/*.xml typeAliasesPackage: com.damon.*.modelspring: profiles: active: dev 最后,我们添加启动类: 123456789@Configuration@EnableAutoConfiguration@ComponentScan(basePackages = {\"com.damon\"})@EnableDiscoveryClientpublic class CasApp { public static void main(String[] args) { SpringApplication.run(CasApp.class, args); }} 以上,一个认证中心的代码实战逻辑就完成了。 接下来,我们看一个客户端如何去认证,首先还是依赖: 123456789101112131415161718192021222324<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId></dependency><dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId></dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId></dependency> 在客户端,我们也需要配置一个资源配置与权限配置: 12345678910111213141516171819202122232425262728293031323334package com.damon.config;import org.springframework.context.annotation.Configuration;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;/** * * * @author Damon * */@Configuration@EnableResourceServerpublic class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.csrf().disable() .exceptionHandling() .authenticationEntryPoint(new AuthenticationEntryPointHandle()) //.authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED)) .and() .requestMatchers().antMatchers(\"/api/**\") .and() .authorizeRequests() .antMatchers(\"/api/**\").authenticated() .and() .httpBasic(); }} 当然,权限拦截可能就相对简单了: 123456789101112131415161718package com.damon.config;import org.springframework.context.annotation.Configuration;import org.springframework.core.annotation.Order;import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;/** * * 在接口上配置权限时使用 * @author Damon * */@Configuration@EnableGlobalMethodSecurity(prePostEnabled = true)@Order(101)public class SecurityConfig extends WebSecurityConfigurerAdapter {} 同样,这里也需要一个统一结果处理类,这里就不展示了。 接下来,我们主要看配置: 123456789101112131415161718192021cas-server-url: http://oauth-cas #http://localhost:2000#设置可以访问的地址security: oauth2: #与cas对应的配置 client: client-id: provider-service client-secret: provider-service-123 user-authorization-uri: ${cas-server-url}/oauth/authorize #是授权码认证方式需要的 access-token-uri: ${cas-server-url}/oauth/token #是密码模式需要用到的获取 token 的接口 resource: loadBalanced: true #jwt: #jwt存储token时开启 #key-uri: ${cas-server-url}/oauth/token_key #key-value: test_jwt_sign_key id: provider-service #指定用户信息地址 user-info-uri: ${cas-server-url}/api/user #指定user info的URI,原生地址后缀为/auth/user prefer-token-info: false #token-info-uri: authorization: check-token-access: ${cas-server-url}/oauth/check_token #当此web服务端接收到来自UI客户端的请求后,需要拿着请求中的 token 到认证服务端做 token 验证,就是请求的这个接口 在上面的配置里,我们看到了各种注释了,讲得很仔细,但是我要强调下:为了高可用,我们的认证中心可能多个,所以需要域名来作 LB。同时,开启了 loadBalanced=true。最后,如果是授权码认证模式,则需要 “user-authorization-uri”,如果是密码模式,需要 “access-token-uri” 来获取 token。我们通过它 “user-info-uri” 来获取认证中心的用户信息,从而判断该用户的权限,从而访问相应的资源。另外,上面的配置需要在 bootstrap 文件中,否则可能失败,大家可以试试。 接下来,我们添加一般配置: 12345678910111213141516171819202122232425262728293031323334353637server: port: 2001 undertow: uri-encoding: UTF-8 accesslog: enabled: false pattern: combined servlet: session: timeout: PT120M cookie: name: PROVIDER-SERVICE-SESSIONID #防止Cookie冲突,冲突会导致登录验证不通过backend: ribbon: client: enabled: true ServerListRefreshInterval: 5000ribbon: ConnectTimeout: 3000 # 设置全局默认的ribbon的读超时 ReadTimeout: 1000 eager-load: enabled: true clients: oauth-cas,consumer-service MaxAutoRetries: 1 #对第一次请求的服务的重试次数 MaxAutoRetriesNextServer: 1 #要重试的下一个服务的最大数量(不包括第一个服务) #listOfServers: localhost:5556,localhost:5557 #ServerListRefreshInterval: 2000 OkToRetryOnAllOperations: true NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRulehystrix.command.BackendCall.execution.isolation.thread.timeoutInMilliseconds: 5000hystrix.threadpool.BackendCallThread.coreSize: 5 这里,我们使用了 Ribbon 来做 LB,hystrix 来作熔断,最后需要注意的是:加上了 cookie name,防止 Cookie 冲突,冲突会导致登录验证不通过。 配置启动类: 123456789101112@Configuration@EnableAutoConfiguration@ComponentScan(basePackages = {\"com.damon\"})@EnableDiscoveryClient@EnableOAuth2Ssopublic class ProviderApp { public static void main(String[] args) { SpringApplication.run(ProviderApp.class, args); }} 我们在上面配置了所有带有 “/api/**“ 的路径请求,都会加以拦截,根据用户的信息来判断其是否有权限访问。 写一个简单的测试类: 12345678910111213141516@RestController@RequestMapping(\"/api/user\")public class UserController { private static final Logger logger = LoggerFactory.getLogger(UserController.class); @Autowired private UserService userService; @PreAuthorize(\"hasAuthority('admin')\") @GetMapping(\"/auth/admin\") public Object adminAuth() { logger.info(\"test password mode\"); return \"Has admin auth!\"; }} 上面的代码表示:如果用户具有 “admin” 的权限,则能够访问该接口,否则会被拒绝。 本文用的是 alibaba 的组件来作 LB,具体可以看前面的文章,用域名来找到服务。同时也加上了网关 Gateway。 最后,我们先来通过密码模式来进行认证吧: 1curl -i -X POST -d \"username=admin&password=123456&grant_type=password&client_id=provider-service&client_secret=provider-service-123\" http://localhost:5555/oauth-cas/oauth/token 认证成功后,会返回如下结果: 1{\"access_token\":\"d2066f68-665b-4038-9dbe-5dd1035e75a0\",\"token_type\":\"bearer\",\"refresh_token\":\"44009836-731c-4e6a-9cc3-274ce3af8c6b\",\"expires_in\":3599,\"scope\":\"all\"} 接下来,我们通过 token 来访问接口: 1curl -i -H \"Accept: application/json\" -H \"Authorization:bearer d2066f68-665b-4038-9dbe-5dd1035e75a0\" -X GET http://localhost:5555/provider-service/api/user/auth/admin 成功会返回结果: 1Has admin auth! token 如果失效,会返回: 1{\"error\":\"invalid_token\",\"error_description\":\"d2066f68-665b-4038-9dbe-5dd1035e75a01\"} 3、GitHub 的授权应用案例 如果你的应用想要接入 GitHub,则可以通过如下办法来实现。 首先注册一个 GitHub 账号,登陆后,找到设置,打开页面,最下面有一个开发者设置: 找到后,点击,可以看到三个,可以选择第二个方式来接入: 可以新增你的应用 app,新建时,应用名、回调地址必填项: 最后,完成后会生成一个 Client ID、Client Secret。 然后利用 Github 官方给的文档来进行认证、接入,授权逻辑: 1.在注册完信息后生成了 Client ID、Client Secret,首先,用户点击 github 登录本地应用引导用户跳转到第三方授权页跳转地址: 1https://github.com/login/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&state={state} 其中,client_id,client_secret 是注册好 Oauth APP 后 github 提供的,需要写在本地代码或者配置文件中,state 也是在本地生成的。redirect_uri 就是在 GitHub 官网填的 Authorization callback URL。此时带着 state 等参数去申请授权,但此时尚未登陆,未能通过 authorize,GitHub 返回 code 参数。 2.授权成功后会重定向带参数访问上面的 redirect_uri,并多了一个 code 参数后台接收 code 这个参数,我们带着这个 code 再次访问 github 地址: 1https://github.com/login/oauth/access_token?client_id=xxx&client_secret=xxx&code=xxx&redirect_uri=http://localhost:3001/authCallback 注意:上面的 redirect_uri 要与之前在新建 app 时填写的保持一直,否则会报错。 3.通过 state 参数和 code 参数,成功获取 access_token有了 access_token,只需要把 access_token 参数放在 URL 后面即可,就可以换取用户信息了。访问地址: 1https://api.github.com/user?access_token=xxx 4.得到 GitHub 授权用户的个人信息,就表明授权成功。 4、微服务安全架构设计 在微服务中,安全性是一个很重要的问题。我们经常比较多的场景是:服务 A 需要调用服务 B,但是问题来了,到底是走外网调用呢?还是走局域网调用呢?这当然看 A、B 是否在同一个网段,如果在同一个局域网段,那肯定走局域网好。为什么呢?因为局域网快呀,如果说还有理由吗?当然有:除了网络快,降低网络开销,还可以保证安全性,不至于被黑客黑掉。这是安全的一个保证。 那么除了上面说的安全性,还有其他的吗?比如:在一个局域网下,有 N 个微服务模块,但是这些微服务并不想完全直接暴露给外部,这时候,就需要一个网关 Gateway 来处理。网关把所有的服务给路由了,就像在所有的服务上面一层,加了一个保护光环,突出高内聚的含义。同时还可以加上一些拦截,安全的拦截,鉴权、认证等。存在通过 token 的鉴权,也可以通过 jwt 的,等等。有时候,可以借助 redis 通过 session 共享。也可以通过 OAuth2 的鉴权模式来实现安全拦截。 最后安全性的考虑是在每个服务的接口设计上,比如:幂等的存在,让很多恶意攻击成为无用之功。更多的介绍可以看下面: 1https://mp.weixin.qq.com/s/G3yhwvLVTu_T5uPxgZD00w 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"后端","slug":"后端","permalink":"http://damon008.github.io/tags/%E5%90%8E%E7%AB%AF/"}]},{"title":"k8s master机器文件系统故障的一次恢复过程","date":"2020-07-23T07:23:59.000Z","path":"2020/07/23/k8s-01/","text":"研发反馈他们那边一套集群有台master文件系统损坏无法开机,他们是三台openstack上的虚机,是虚拟化宿主机故障导致的虚机文件系统损坏。三台机器是master+node,指导他修复后开机,修复过程和我之前文章opensuse的一次救援步骤一样 起来后我上去看,因为做了 HA 的,所以只有这个node有问题,集群没影响 12345[root@k8s-m1 ~]# kubectl get node -o wideNAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME10.252.146.104 NotReady <none> 30d v1.16.9 10.252.146.104 <none> CentOS Linux 8 (Core) 4.18.0-193.6.3.el8_2.x86_64 docker://19.3.1110.252.146.105 Ready <none> 30d v1.16.9 10.252.146.105 <none> CentOS Linux 8 (Core) 4.18.0-193.6.3.el8_2.x86_64 docker://19.3.1110.252.146.106 Ready <none> 30d v1.16.9 10.252.146.106 <none> CentOS Linux 8 (Core) 4.18.0-193.6.3.el8_2.x86_64 docker://19.3.11 启动docker试试 12[root@k8s-m1 ~]# systemctl start dockerJob for docker.service canceled. 无法启动,查看下启动失败的服务 123[root@k8s-m1 ~]# systemctl --failed UNIT LOAD ACTIVE SUB DESCRIPTION● containerd.service loaded failed failed containerd container runtime 查看下containerd的日志 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849[root@k8s-m1 ~]# journalctl -xe -u containerdJul 23 11:20:11 k8s-m1 containerd[9186]: time=\"2020-07-23T11:20:11.481459735+08:00\" level=info msg=\"loading plugin \"io.containerd.service.v1.snapshots-service\"...\" type=io.containerd.service.v1Jul 23 11:20:11 k8s-m1 containerd[9186]: time=\"2020-07-23T11:20:11.481472223+08:00\" level=info msg=\"loading plugin \"io.containerd.runtime.v1.linux\"...\" type=io.containerd.runtime.v1Jul 23 11:20:11 k8s-m1 containerd[9186]: time=\"2020-07-23T11:20:11.481517630+08:00\" level=info msg=\"loading plugin \"io.containerd.runtime.v2.task\"...\" type=io.containerd.runtime.v2Jul 23 11:20:11 k8s-m1 containerd[9186]: time=\"2020-07-23T11:20:11.481562176+08:00\" level=info msg=\"loading plugin \"io.containerd.monitor.v1.cgroups\"...\" type=io.containerd.monitor.v1Jul 23 11:20:11 k8s-m1 containerd[9186]: time=\"2020-07-23T11:20:11.481964349+08:00\" level=info msg=\"loading plugin \"io.containerd.service.v1.tasks-service\"...\" type=io.containerd.service.v1Jul 23 11:20:11 k8s-m1 containerd[9186]: time=\"2020-07-23T11:20:11.481996158+08:00\" level=info msg=\"loading plugin \"io.containerd.internal.v1.restart\"...\" type=io.containerd.internal.v1Jul 23 11:20:11 k8s-m1 containerd[9186]: time=\"2020-07-23T11:20:11.482048208+08:00\" level=info msg=\"loading plugin \"io.containerd.grpc.v1.containers\"...\" type=io.containerd.grpc.v1Jul 23 11:20:11 k8s-m1 containerd[9186]: time=\"2020-07-23T11:20:11.482081110+08:00\" level=info msg=\"loading plugin \"io.containerd.grpc.v1.content\"...\" type=io.containerd.grpc.v1Jul 23 11:20:11 k8s-m1 containerd[9186]: time=\"2020-07-23T11:20:11.482096598+08:00\" level=info msg=\"loading plugin \"io.containerd.grpc.v1.diff\"...\" type=io.containerd.grpc.v1Jul 23 11:20:11 k8s-m1 containerd[9186]: time=\"2020-07-23T11:20:11.482112263+08:00\" level=info msg=\"loading plugin \"io.containerd.grpc.v1.events\"...\" type=io.containerd.grpc.v1Jul 23 11:20:11 k8s-m1 containerd[9186]: time=\"2020-07-23T11:20:11.482123307+08:00\" level=info msg=\"loading plugin \"io.containerd.grpc.v1.healthcheck\"...\" type=io.containerd.grpc.v1Jul 23 11:20:11 k8s-m1 containerd[9186]: time=\"2020-07-23T11:20:11.482133477+08:00\" level=info msg=\"loading plugin \"io.containerd.grpc.v1.images\"...\" type=io.containerd.grpc.v1Jul 23 11:20:11 k8s-m1 containerd[9186]: time=\"2020-07-23T11:20:11.482142943+08:00\" level=info msg=\"loading plugin \"io.containerd.grpc.v1.leases\"...\" type=io.containerd.grpc.v1Jul 23 11:20:11 k8s-m1 containerd[9186]: time=\"2020-07-23T11:20:11.482151644+08:00\" level=info msg=\"loading plugin \"io.containerd.grpc.v1.namespaces\"...\" type=io.containerd.grpc.v1Jul 23 11:20:11 k8s-m1 containerd[9186]: time=\"2020-07-23T11:20:11.482160741+08:00\" level=info msg=\"loading plugin \"io.containerd.internal.v1.opt\"...\" type=io.containerd.internal.v1Jul 23 11:20:11 k8s-m1 containerd[9186]: time=\"2020-07-23T11:20:11.482184201+08:00\" level=info msg=\"loading plugin \"io.containerd.grpc.v1.snapshots\"...\" type=io.containerd.grpc.v1Jul 23 11:20:11 k8s-m1 containerd[9186]: time=\"2020-07-23T11:20:11.482194643+08:00\" level=info msg=\"loading plugin \"io.containerd.grpc.v1.tasks\"...\" type=io.containerd.grpc.v1Jul 23 11:20:11 k8s-m1 containerd[9186]: time=\"2020-07-23T11:20:11.482206871+08:00\" level=info msg=\"loading plugin \"io.containerd.grpc.v1.version\"...\" type=io.containerd.grpc.v1Jul 23 11:20:11 k8s-m1 containerd[9186]: time=\"2020-07-23T11:20:11.482215454+08:00\" level=info msg=\"loading plugin \"io.containerd.grpc.v1.introspection\"...\" type=io.containerd.grpc.v1Jul 23 11:20:11 k8s-m1 containerd[9186]: time=\"2020-07-23T11:20:11.482365838+08:00\" level=info msg=serving... address=\"/run/containerd/containerd.sock\"Jul 23 11:20:11 k8s-m1 containerd[9186]: time=\"2020-07-23T11:20:11.482404139+08:00\" level=info msg=\"containerd successfully booted in 0.003611s\"Jul 23 11:20:11 k8s-m1 containerd[9186]: panic: runtime error: invalid memory address or nil pointer dereferenceJul 23 11:20:11 k8s-m1 containerd[9186]: [signal SIGSEGV: segmentation violation code=0x1 addr=0x8 pc=0x5626b983c259]Jul 23 11:20:11 k8s-m1 containerd[9186]: goroutine 55 [running]:Jul 23 11:20:11 k8s-m1 containerd[9186]: github.com/containerd/containerd/vendor/go.etcd.io/bbolt.(*Bucket).Cursor(...)Jul 23 11:20:11 k8s-m1 containerd[9186]: /go/src/github.com/containerd/containerd/vendor/go.etcd.io/bbolt/bucket.go:84Jul 23 11:20:11 k8s-m1 containerd[9186]: github.com/containerd/containerd/vendor/go.etcd.io/bbolt.(*Bucket).Get(0x0, 0x5626bb7e3f10, 0xb, 0xb, 0x0, 0x2, 0x4)Jul 23 11:20:11 k8s-m1 containerd[9186]: /go/src/github.com/containerd/containerd/vendor/go.etcd.io/bbolt/bucket.go:260 +0x39Jul 23 11:20:11 k8s-m1 containerd[9186]: github.com/containerd/containerd/metadata.scanRoots.func6(0x7fe557c63020, 0x2, 0x2, 0x0, 0x0, 0x0, 0x0, 0x5626b95eec72)Jul 23 11:20:11 k8s-m1 containerd[9186]: /go/src/github.com/containerd/containerd/metadata/gc.go:222 +0xcbJul 23 11:20:11 k8s-m1 containerd[9186]: github.com/containerd/containerd/vendor/go.etcd.io/bbolt.(*Bucket).ForEach(0xc0003d1780, 0xc00057b640, 0xa, 0xa)Jul 23 11:20:11 k8s-m1 containerd[9186]: /go/src/github.com/containerd/containerd/vendor/go.etcd.io/bbolt/bucket.go:388 +0x100Jul 23 11:20:11 k8s-m1 containerd[9186]: github.com/containerd/containerd/metadata.scanRoots(0x5626bacedde0, 0xc0003d1680, 0xc0002ee2a0, 0xc00031a3c0, 0xc000527a60, 0x7fe586a43fff)Jul 23 11:20:11 k8s-m1 containerd[9186]: /go/src/github.com/containerd/containerd/metadata/gc.go:216 +0x4dfJul 23 11:20:11 k8s-m1 containerd[9186]: github.com/containerd/containerd/metadata.(*DB).getMarked.func1(0xc0002ee2a0, 0x0, 0x0)Jul 23 11:20:11 k8s-m1 containerd[9186]: /go/src/github.com/containerd/containerd/metadata/db.go:359 +0x165Jul 23 11:20:11 k8s-m1 containerd[9186]: github.com/containerd/containerd/vendor/go.etcd.io/bbolt.(*DB).View(0xc00000c1e0, 0xc00008b860, 0x0, 0x0)Jul 23 11:20:11 k8s-m1 containerd[9186]: /go/src/github.com/containerd/containerd/vendor/go.etcd.io/bbolt/db.go:701 +0x92Jul 23 11:20:11 k8s-m1 containerd[9186]: github.com/containerd/containerd/metadata.(*DB).getMarked(0xc0000a0a80, 0x5626bacede20, 0xc0000d6010, 0x203000, 0x203000, 0x400)Jul 23 11:20:11 k8s-m1 containerd[9186]: /go/src/github.com/containerd/containerd/metadata/db.go:342 +0x7eJul 23 11:20:11 k8s-m1 containerd[9186]: github.com/containerd/containerd/metadata.(*DB).GarbageCollect(0xc0000a0a80, 0x5626bacede20, 0xc0000d6010, 0x0, 0x1, 0x0, 0x0)Jul 23 11:20:11 k8s-m1 containerd[9186]: /go/src/github.com/containerd/containerd/metadata/db.go:257 +0xa3Jul 23 11:20:11 k8s-m1 containerd[9186]: github.com/containerd/containerd/gc/scheduler.(*gcScheduler).run(0xc0000a0b40, 0x5626bacede20, 0xc0000d6010)Jul 23 11:20:11 k8s-m1 containerd[9186]: /go/src/github.com/containerd/containerd/gc/scheduler/scheduler.go:310 +0x511Jul 23 11:20:11 k8s-m1 containerd[9186]: created by github.com/containerd/containerd/gc/scheduler.init.0.func1Jul 23 11:20:11 k8s-m1 containerd[9186]: /go/src/github.com/containerd/containerd/gc/scheduler/scheduler.go:132 +0x462Jul 23 11:20:11 k8s-m1 systemd[1]: containerd.service: Main process exited, code=exited, status=2/INVALIDARGUMENTJul 23 11:20:11 k8s-m1 systemd[1]: containerd.service: Failed with result 'exit-code'. 这个问题从panic抛出的堆栈信息看和我之前文章docker启动panic很类似,都是 boltdb 文件出错,找下 git 信息去看看代码路径在哪 123456[root@k8s-m1 ~]# systemctl cat containerd | grep ExecStartExecStartPre=-/sbin/modprobe overlayExecStart=/usr/bin/containerd[root@k8s-m1 ~]# /usr/bin/containerd --versioncontainerd containerd.io 1.2.13 7ad184331fa3e55e52b890ea95e65ba581ae3429 按照这个blob去用github的url访问是404,只有去按照tag版本查看了,根据相关代码找到了 boltdb 的文件名是meta.db 123https://github.com/containerd/containerd/blob/v1.2.13/metadata/db.go#L257https://github.com/containerd/containerd/blob/v1.2.13/metadata/db.go#L79https://github.com/containerd/containerd/blob/v1.2.13/services/server/server.go#L261-L268 查找下ic.Root路径是多少 12345678[root@k8s-m1 ~]# /usr/bin/containerd --help | grep config config information on the containerd config --config value, -c value path to the configuration file (default: \"/etc/containerd/config.toml\")[root@k8s-m1 ~]# grep root /etc/containerd/config.toml#root = \"/var/lib/containerd\"[root@k8s-m1 ~]]# find /var/lib/containerd -type f -name meta.db/var/lib/containerd/io.containerd.metadata.v1.bolt/meta.db 找到boltdb文件,改名启动 12345678910111213141516171819202122232425262728293031323334353637383940414243[root@k8s-m1 ~]]# mv /var/lib/containerd/io.containerd.metadata.v1.bolt/meta.db{,.bak}[root@k8s-m1 ~]# systemctl status containerd.service● containerd.service - containerd container runtime Loaded: loaded (/usr/lib/systemd/system/containerd.service; disabled; vendor preset: disabled) Active: failed (Result: exit-code) since Thu 2020-07-23 11:20:11 CST; 17min ago Docs: https://containerd.io Process: 9186 ExecStart=/usr/bin/containerd (code=exited, status=2) Process: 9182 ExecStartPre=/sbin/modprobe overlay (code=exited, status=0/SUCCESS) Main PID: 9186 (code=exited, status=2)Jul 23 11:20:11 k8s-m1 containerd[9186]: github.com/containerd/containerd/metadata.(*DB).getMarked(0xc0000a0a80, 0x5626bacede20, 0xc0000d6010, 0x203000, 0x203000, 0x400)Jul 23 11:20:11 k8s-m1 containerd[9186]: /go/src/github.com/containerd/containerd/metadata/db.go:342 +0x7eJul 23 11:20:11 k8s-m1 containerd[9186]: github.com/containerd/containerd/metadata.(*DB).GarbageCollect(0xc0000a0a80, 0x5626bacede20, 0xc0000d6010, 0x0, 0x1, 0x0, 0x0)Jul 23 11:20:11 k8s-m1 containerd[9186]: /go/src/github.com/containerd/containerd/metadata/db.go:257 +0xa3Jul 23 11:20:11 k8s-m1 containerd[9186]: github.com/containerd/containerd/gc/scheduler.(*gcScheduler).run(0xc0000a0b40, 0x5626bacede20, 0xc0000d6010)Jul 23 11:20:11 k8s-m1 containerd[9186]: /go/src/github.com/containerd/containerd/gc/scheduler/scheduler.go:310 +0x511Jul 23 11:20:11 k8s-m1 containerd[9186]: created by github.com/containerd/containerd/gc/scheduler.init.0.func1Jul 23 11:20:11 k8s-m1 containerd[9186]: /go/src/github.com/containerd/containerd/gc/scheduler/scheduler.go:132 +0x462Jul 23 11:20:11 k8s-m1 systemd[1]: containerd.service: Main process exited, code=exited, status=2/INVALIDARGUMENTJul 23 11:20:11 k8s-m1 systemd[1]: containerd.service: Failed with result 'exit-code'.[root@k8s-m1 ~]# systemctl restart containerd.service[root@k8s-m1 ~]# systemctl status containerd.service● containerd.service - containerd container runtime Loaded: loaded (/usr/lib/systemd/system/containerd.service; disabled; vendor preset: disabled) Active: active (running) since Thu 2020-07-23 11:25:37 CST; 1s ago Docs: https://containerd.io Process: 15661 ExecStartPre=/sbin/modprobe overlay (code=exited, status=0/SUCCESS) Main PID: 15663 (containerd) Tasks: 16 Memory: 28.6M CGroup: /system.slice/containerd.service └─15663 /usr/bin/containerdJul 23 11:25:37 k8s-m1 containerd[15663]: time=\"2020-07-23T11:25:37.496725460+08:00\" level=info msg=\"loading plugin \"io.containerd.grpc.v1.images\"...\" type=io.containerd.grpc.v1Jul 23 11:25:37 k8s-m1 containerd[15663]: time=\"2020-07-23T11:25:37.496734129+08:00\" level=info msg=\"loading plugin \"io.containerd.grpc.v1.leases\"...\" type=io.containerd.grpc.v1Jul 23 11:25:37 k8s-m1 containerd[15663]: time=\"2020-07-23T11:25:37.496742793+08:00\" level=info msg=\"loading plugin \"io.containerd.grpc.v1.namespaces\"...\" type=io.containerd.grpc.v1Jul 23 11:25:37 k8s-m1 containerd[15663]: time=\"2020-07-23T11:25:37.496751740+08:00\" level=info msg=\"loading plugin \"io.containerd.internal.v1.opt\"...\" type=io.containerd.internal.v1Jul 23 11:25:37 k8s-m1 containerd[15663]: time=\"2020-07-23T11:25:37.496775185+08:00\" level=info msg=\"loading plugin \"io.containerd.grpc.v1.snapshots\"...\" type=io.containerd.grpc.v1Jul 23 11:25:37 k8s-m1 containerd[15663]: time=\"2020-07-23T11:25:37.496785498+08:00\" level=info msg=\"loading plugin \"io.containerd.grpc.v1.tasks\"...\" type=io.containerd.grpc.v1Jul 23 11:25:37 k8s-m1 containerd[15663]: time=\"2020-07-23T11:25:37.496794873+08:00\" level=info msg=\"loading plugin \"io.containerd.grpc.v1.version\"...\" type=io.containerd.grpc.v1Jul 23 11:25:37 k8s-m1 containerd[15663]: time=\"2020-07-23T11:25:37.496803178+08:00\" level=info msg=\"loading plugin \"io.containerd.grpc.v1.introspection\"...\" type=io.containerd.grpc.v1Jul 23 11:25:37 k8s-m1 containerd[15663]: time=\"2020-07-23T11:25:37.496944458+08:00\" level=info msg=serving... address=\"/run/containerd/containerd.sock\"Jul 23 11:25:37 k8s-m1 containerd[15663]: time=\"2020-07-23T11:25:37.496958031+08:00\" level=info msg=\"containerd successfully booted in 0.003994s\" containerd 起来后,启动下 docker 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647[root@k8s-m1 ~]# systemctl status docker● docker.service - Docker Application Container Engine Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; vendor preset: disabled) Drop-In: /etc/systemd/system/docker.service.d └─10-docker.conf Active: inactive (dead) since Thu 2020-07-23 11:20:13 CST; 18min ago Docs: https://docs.docker.com Process: 9398 ExecStopPost=/bin/bash -c /sbin/iptables -D FORWARD -s 0.0.0.0/0 -j ACCEPT &> /dev/null || : (code=exited, status=0/SUCCESS) Process: 9187 ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock (code=exited, status=0/SUCCESS) Main PID: 9187 (code=exited, status=0/SUCCESS)Jul 23 11:20:13 k8s-m1 dockerd[9187]: time=\"2020-07-23T11:20:13.956503485+08:00\" level=error msg=\"Stop container error: Failed to stop container 68860c8d16b9ce7e74e8efd9db00e70a57eef1b752c2e6c703073c0bce5517d3 with error: Cannot kill c>Jul 23 11:20:13 k8s-m1 dockerd[9187]: time=\"2020-07-23T11:20:13.954347116+08:00\" level=error msg=\"Stop container error: Failed to stop container 5ec9922beed1276989f1866c3fd911f37cc26aae4e4b27c7ce78183a9a4725cc with error: Cannot kill c>Jul 23 11:20:13 k8s-m1 dockerd[9187]: time=\"2020-07-23T11:20:13.953615411+08:00\" level=info msg=\"Container failed to stop after sending signal 15 to the process, force killing\"Jul 23 11:20:13 k8s-m1 dockerd[9187]: time=\"2020-07-23T11:20:13.956557179+08:00\" level=error msg=\"Stop container error: Failed to stop container 6d0096fbcd4055f8bafb6b38f502a0186cd1dfca34219e9dd6050f512971aef5 with error: Cannot kill c>Jul 23 11:20:13 k8s-m1 dockerd[9187]: time=\"2020-07-23T11:20:13.954601191+08:00\" level=info msg=\"Container failed to stop after sending signal 15 to the process, force killing\"Jul 23 11:20:13 k8s-m1 dockerd[9187]: time=\"2020-07-23T11:20:13.956600790+08:00\" level=error msg=\"Stop container error: Failed to stop container 6d1175ba6c55cb05ad89f4134ba8e9d3495c5acb5f07938dc16339b7cca013bf with error: Cannot kill c>Jul 23 11:20:13 k8s-m1 dockerd[9187]: time=\"2020-07-23T11:20:13.957188989+08:00\" level=info msg=\"Daemon shutdown complete\"Jul 23 11:20:13 k8s-m1 dockerd[9187]: time=\"2020-07-23T11:20:13.957212655+08:00\" level=info msg=\"stopping event stream following graceful shutdown\" error=\"context canceled\" module=libcontainerd namespace=plugins.mobyJul 23 11:20:13 k8s-m1 dockerd[9187]: time=\"2020-07-23T11:20:13.957209679+08:00\" level=info msg=\"stopping event stream following graceful shutdown\" error=\"context canceled\" module=libcontainerd namespace=mobyJul 23 11:20:13 k8s-m1 systemd[1]: Stopped Docker Application Container Engine.[root@k8s-m1 ~]# systemctl start docker[root@k8s-m1 ~]# systemctl status docker● docker.service - Docker Application Container Engine Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; vendor preset: disabled) Drop-In: /etc/systemd/system/docker.service.d └─10-docker.conf Active: active (running) since Thu 2020-07-23 11:26:11 CST; 1s ago Docs: https://docs.docker.com Process: 9398 ExecStopPost=/bin/bash -c /sbin/iptables -D FORWARD -s 0.0.0.0/0 -j ACCEPT &> /dev/null || : (code=exited, status=0/SUCCESS) Process: 16156 ExecStartPost=/sbin/iptables -I FORWARD -s 0.0.0.0/0 -j ACCEPT (code=exited, status=0/SUCCESS) Main PID: 15974 (dockerd) Tasks: 62 Memory: 89.1M CGroup: /system.slice/docker.service └─15974 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sockJul 23 11:26:10 k8s-m1 dockerd[15974]: time=\"2020-07-23T11:26:10.851106564+08:00\" level=error msg=\"cb4e16249cd8eac48ed734c71237195f04d63c56c55c0199b3cdf3d49461903d cleanup: failed to delete container from containerd: no such container\"Jul 23 11:26:10 k8s-m1 dockerd[15974]: time=\"2020-07-23T11:26:10.860456898+08:00\" level=error msg=\"d9bbcab186ccb59f96c95fc886ec1b66a52aa96e45b117cf7d12e3ff9b95db9f cleanup: failed to delete container from containerd: no such container\"Jul 23 11:26:10 k8s-m1 dockerd[15974]: time=\"2020-07-23T11:26:10.872405757+08:00\" level=error msg=\"07eb7a09bc8589abcb4d79af4b46798327bfb00624a7b9ceea457de392ad8f3d cleanup: failed to delete container from containerd: no such container\"Jul 23 11:26:10 k8s-m1 dockerd[15974]: time=\"2020-07-23T11:26:10.877896618+08:00\" level=error msg=\"f5867657025bd7c3951cbd3e08ad97338cf69df2a97967a419e0e78eda869b73 cleanup: failed to delete container from containerd: no such container\"Jul 23 11:26:11 k8s-m1 dockerd[15974]: time=\"2020-07-23T11:26:11.143661583+08:00\" level=info msg=\"Default bridge (docker0) is assigned with an IP address 172.17.0.0/16. Daemon option --bip can be used to set a preferred IP address\"Jul 23 11:26:11 k8s-m1 dockerd[15974]: time=\"2020-07-23T11:26:11.198200760+08:00\" level=info msg=\"Loading containers: done.\"Jul 23 11:26:11 k8s-m1 dockerd[15974]: time=\"2020-07-23T11:26:11.219959208+08:00\" level=info msg=\"Docker daemon\" commit=42e35e61f3 graphdriver(s)=overlay2 version=19.03.11Jul 23 11:26:11 k8s-m1 dockerd[15974]: time=\"2020-07-23T11:26:11.220049865+08:00\" level=info msg=\"Daemon has completed initialization\"Jul 23 11:26:11 k8s-m1 dockerd[15974]: time=\"2020-07-23T11:26:11.232373131+08:00\" level=info msg=\"API listen on /var/run/docker.sock\"Jul 23 11:26:11 k8s-m1 systemd[1]: Started Docker Application Container Engine. etcd启动也失败,journal 查看下 etcd 状态 123456789101112131415161718192021222324252627282930[root@k8s-m1 ~]# journalctl -xe -u etcdJul 23 11:26:15 k8s-m1 etcd[18129]: Loading server configuration from \"/etc/etcd/etcd.config.yml\"Jul 23 11:26:15 k8s-m1 etcd[18129]: etcd Version: 3.3.20Jul 23 11:26:15 k8s-m1 etcd[18129]: Git SHA: 9fd7e2b80Jul 23 11:26:15 k8s-m1 etcd[18129]: Go Version: go1.12.17Jul 23 11:26:15 k8s-m1 etcd[18129]: Go OS/Arch: linux/amd64Jul 23 11:26:15 k8s-m1 etcd[18129]: setting maximum number of CPUs to 16, total number of available CPUs is 16Jul 23 11:26:15 k8s-m1 etcd[18129]: found invalid file/dir wal under data dir /var/lib/etcd (Ignore this if you are upgrading etcd)Jul 23 11:26:15 k8s-m1 etcd[18129]: the server is already initialized as member before, starting as etcd member...Jul 23 11:26:15 k8s-m1 etcd[18129]: ignoring peer auto TLS since certs givenJul 23 11:26:15 k8s-m1 etcd[18129]: peerTLS: cert = /etc/kubernetes/pki/etcd/peer.crt, key = /etc/kubernetes/pki/etcd/peer.key, ca = /etc/kubernetes/pki/etcd/ca.crt, trusted-ca = /etc/kubernetes/pki/etcd/ca.crt, client-cert-auth = fals>Jul 23 11:26:15 k8s-m1 etcd[18129]: listening for peers on https://10.252.146.104:2380Jul 23 11:26:15 k8s-m1 etcd[18129]: ignoring client auto TLS since certs givenJul 23 11:26:15 k8s-m1 etcd[18129]: pprof is enabled under /debug/pprofJul 23 11:26:15 k8s-m1 etcd[18129]: The scheme of client url http://127.0.0.1:2379 is HTTP while peer key/cert files are presented. Ignored key/cert files.Jul 23 11:26:15 k8s-m1 etcd[18129]: The scheme of client url http://127.0.0.1:2379 is HTTP while client cert auth (--client-cert-auth) is enabled. Ignored client cert auth for this url.Jul 23 11:26:15 k8s-m1 etcd[18129]: listening for client requests on 127.0.0.1:2379Jul 23 11:26:15 k8s-m1 etcd[18129]: listening for client requests on 10.252.146.104:2379Jul 23 11:26:15 k8s-m1 etcd[18129]: skipped unexpected non snapshot file 000000000000002e-000000000052f2be.snap.brokenJul 23 11:26:15 k8s-m1 etcd[18129]: recovered store from snapshot at index 5426092Jul 23 11:26:15 k8s-m1 etcd[18129]: restore compact to 3967425Jul 23 11:26:15 k8s-m1 etcd[18129]: cannot unmarshal event: proto: KeyValue: illegal tag 0 (wire type 0)Jul 23 11:26:15 k8s-m1 systemd[1]: etcd.service: Main process exited, code=exited, status=1/FAILUREJul 23 11:26:15 k8s-m1 systemd[1]: etcd.service: Failed with result 'exit-code'.Jul 23 11:26:15 k8s-m1 systemd[1]: Failed to start Etcd Service.[root@k8s-m1 ~]# ll /var/lib/etcd/member/snap/total 8560-rw-r--r-- 1 root root 13499 Jul 20 13:36 000000000000002e-000000000052cbac.snap-rw-r--r-- 2 root root 128360 Jul 20 13:01 000000000000002e-000000000052f2be.snap.broken-rw------- 1 root root 8617984 Jul 23 11:26 db 这套集群是使用我的ansible部署,求star的,自带了备份脚本,但是是三天前坏的 123456[root@k8s-m1 ~]# ll /opt/etcd_bak/total 41524-rw-r--r-- 1 root root 8618016 Jul 17 02:00 etcd-2020-07-17-02:00:01.db-rw-r--r-- 1 root root 8618016 Jul 18 02:00 etcd-2020-07-18-02:00:01.db-rw-r--r-- 1 root root 8323104 Jul 19 02:00 etcd-2020-07-19-02:00:01.db-rw-r--r-- 1 root root 8618016 Jul 20 02:00 etcd-2020-07-20-02:00:01.db 有恢复剧本,但是前提是etcd的v2和v3不能共存,否则无法恢复备份,我们线上都是把v2的存储关闭了的。主要是这个tasks里的26到42行步骤,这里复制了其他机器master上的 07/23 号的etcd备份文件,然后改了下host跑了下 12345678910111213141516171819202122232425262728293031323334353637383940[root@k8s-m1 ~]# cd Kubernetes-ansible[root@k8s-m1 Kubernetes-ansible]# ansible-playbook restoreETCD.yml -e 'db=/opt/etcd_bak/etcd-bak.db'PLAY [10.252.146.104] **********************************************************************************************************************************************************************************************************************TASK [Gathering Facts] *********************************************************************************************************************************************************************************************************************ok: [10.252.146.104]TASK [restoreETCD : fail] ******************************************************************************************************************************************************************************************************************skipping: [10.252.146.104]TASK [restoreETCD : 检测备份文件存在否] *************************************************************************************************************************************************************************************************************ok: [10.252.146.104]TASK [restoreETCD : fail] ******************************************************************************************************************************************************************************************************************skipping: [10.252.146.104]TASK [restoreETCD : set_fact] **************************************************************************************************************************************************************************************************************skipping: [10.252.146.104]TASK [restoreETCD : set_fact] **************************************************************************************************************************************************************************************************************ok: [10.252.146.104]TASK [restoreETCD : 停止etcd] ****************************************************************************************************************************************************************************************************************ok: [10.252.146.104]TASK [restoreETCD : 删除etcd数据目录] ************************************************************************************************************************************************************************************************************ok: [10.252.146.104] => (item=/var/lib/etcd)TASK [restoreETCD : 分发备份文件] ****************************************************************************************************************************************************************************************************************ok: [10.252.146.104]TASK [restoreETCD : 恢复备份] ******************************************************************************************************************************************************************************************************************changed: [10.252.146.104]TASK [restoreETCD : 启动etcd] ****************************************************************************************************************************************************************************************************************fatal: [10.252.146.104]: FAILED! => {\"changed\": false, \"msg\": \"Unable to start service etcd: Job for etcd.service failed because the control process exited with error code.\\nSee \\\"systemctl status etcd.service\\\" and \\\"journalctl -xe\\\" for details.\\n\"}PLAY RECAP *********************************************************************************************************************************************************************************************************************************10.252.146.104 : ok=7 changed=1 unreachable=0 failed=1 skipped=3 rescued=0 ignored=0 查看下日志 123456789101112131415161718192021[root@k8s-m1 Kubernetes-ansible]# journalctl -xe -u etcdJul 23 11:27:46 k8s-m1 etcd[58954]: Loading server configuration from \"/etc/etcd/etcd.config.yml\"Jul 23 11:27:46 k8s-m1 etcd[58954]: etcd Version: 3.3.20Jul 23 11:27:46 k8s-m1 etcd[58954]: Git SHA: 9fd7e2b80Jul 23 11:27:46 k8s-m1 etcd[58954]: Go Version: go1.12.17Jul 23 11:27:46 k8s-m1 etcd[58954]: Go OS/Arch: linux/amd64Jul 23 11:27:46 k8s-m1 etcd[58954]: setting maximum number of CPUs to 16, total number of available CPUs is 16Jul 23 11:27:46 k8s-m1 etcd[58954]: the server is already initialized as member before, starting as etcd member...Jul 23 11:27:46 k8s-m1 etcd[58954]: ignoring peer auto TLS since certs givenJul 23 11:27:46 k8s-m1 etcd[58954]: peerTLS: cert = /etc/kubernetes/pki/etcd/peer.crt, key = /etc/kubernetes/pki/etcd/peer.key, ca = /etc/kubernetes/pki/etcd/ca.crt, trusted-ca = /etc/kubernetes/pki/etcd/ca.crt, client-cert-auth = fals>Jul 23 11:27:46 k8s-m1 etcd[58954]: listening for peers on https://10.252.146.104:2380Jul 23 11:27:46 k8s-m1 etcd[58954]: ignoring client auto TLS since certs givenJul 23 11:27:46 k8s-m1 etcd[58954]: pprof is enabled under /debug/pprofJul 23 11:27:46 k8s-m1 etcd[58954]: The scheme of client url http://127.0.0.1:2379 is HTTP while peer key/cert files are presented. Ignored key/cert files.Jul 23 11:27:46 k8s-m1 etcd[58954]: The scheme of client url http://127.0.0.1:2379 is HTTP while client cert auth (--client-cert-auth) is enabled. Ignored client cert auth for this url.Jul 23 11:27:46 k8s-m1 etcd[58954]: listening for client requests on 127.0.0.1:2379Jul 23 11:27:46 k8s-m1 etcd[58954]: listening for client requests on 10.252.146.104:2379Jul 23 11:27:47 k8s-m1 etcd[58954]: member ac2dcf6aed12e8f1 has already been bootstrappedJul 23 11:27:47 k8s-m1 systemd[1]: etcd.service: Main process exited, code=exited, status=1/FAILUREJul 23 11:27:47 k8s-m1 systemd[1]: etcd.service: Failed with result 'exit-code'.Jul 23 11:27:47 k8s-m1 systemd[1]: Failed to start Etcd Service. 这个member xxxx has already been bootstrapped解决办法就是把配置文件的下面修改,后面启动完记得改回来 1initial-cluster-state: 'new' 改成 initial-cluster-state: 'existing' 然后成功启动 123456789101112131415161718192021222324252627282930313233343536[root@k8s-m1 Kubernetes-ansible]# systemctl start etcd[root@k8s-m1 Kubernetes-ansible]# journalctl -xe -u etcdJul 23 11:27:55 k8s-m1 etcd[59889]: Loading server configuration from \"/etc/etcd/etcd.config.yml\"Jul 23 11:27:55 k8s-m1 etcd[59889]: etcd Version: 3.3.20Jul 23 11:27:55 k8s-m1 etcd[59889]: Git SHA: 9fd7e2b80Jul 23 11:27:55 k8s-m1 etcd[59889]: Go Version: go1.12.17Jul 23 11:27:55 k8s-m1 etcd[59889]: Go OS/Arch: linux/amd64Jul 23 11:27:55 k8s-m1 etcd[59889]: setting maximum number of CPUs to 16, total number of available CPUs is 16Jul 23 11:27:55 k8s-m1 etcd[59889]: found invalid file/dir wal under data dir /var/lib/etcd (Ignore this if you are upgrading etcd)Jul 23 11:27:55 k8s-m1 etcd[59889]: the server is already initialized as member before, starting as etcd member...Jul 23 11:27:55 k8s-m1 etcd[59889]: ignoring peer auto TLS since certs givenJul 23 11:27:55 k8s-m1 etcd[59889]: peerTLS: cert = /etc/kubernetes/pki/etcd/peer.crt, key = /etc/kubernetes/pki/etcd/peer.key, ca = /etc/kubernetes/pki/etcd/ca.crt, trusted-ca = /etc/kubernetes/pki/etcd/ca.crt, client-cert-auth = fals>Jul 23 11:27:55 k8s-m1 etcd[59889]: listening for peers on https://10.252.146.104:2380Jul 23 11:27:55 k8s-m1 etcd[59889]: ignoring client auto TLS since certs givenJul 23 11:27:55 k8s-m1 etcd[59889]: pprof is enabled under /debug/pprofJul 23 11:27:55 k8s-m1 etcd[59889]: The scheme of client url http://127.0.0.1:2379 is HTTP while peer key/cert files are presented. Ignored key/cert files.Jul 23 11:27:55 k8s-m1 etcd[59889]: The scheme of client url http://127.0.0.1:2379 is HTTP while client cert auth (--client-cert-auth) is enabled. Ignored client cert auth for this url.Jul 23 11:27:55 k8s-m1 etcd[59889]: listening for client requests on 127.0.0.1:2379Jul 23 11:27:55 k8s-m1 etcd[59889]: listening for client requests on 10.252.146.104:2379Jul 23 11:27:55 k8s-m1 etcd[59889]: recovered store from snapshot at index 5952463Jul 23 11:27:55 k8s-m1 etcd[59889]: restore compact to 4369703Jul 23 11:27:55 k8s-m1 etcd[59889]: name = etcd-001Jul 23 11:27:55 k8s-m1 etcd[59889]: data dir = /var/lib/etcdJul 23 11:27:55 k8s-m1 etcd[59889]: member dir = /var/lib/etcd/memberJul 23 11:27:55 k8s-m1 etcd[59889]: dedicated WAL dir = /var/lib/etcd/walJul 23 11:27:55 k8s-m1 etcd[59889]: heartbeat = 100msJul 23 11:27:55 k8s-m1 etcd[59889]: election = 1000msJul 23 11:27:55 k8s-m1 etcd[59889]: snapshot count = 5000Jul 23 11:27:55 k8s-m1 etcd[59889]: advertise client URLs = https://10.252.146.104:2379Jul 23 11:27:55 k8s-m1 etcd[59889]: restarting member ac2dcf6aed12e8f1 in cluster 367e2aebc6430cbe at commit index 5952491Jul 23 11:27:55 k8s-m1 etcd[59889]: ac2dcf6aed12e8f1 became follower at term 47Jul 23 11:27:55 k8s-m1 etcd[59889]: newRaft ac2dcf6aed12e8f1 [peers: [1e713be314744d53,8b1621b475555fd9,ac2dcf6aed12e8f1], term: 47, commit: 5952491, applied: 5952463, lastindex: 5952491, lastterm: 47]Jul 23 11:27:55 k8s-m1 etcd[59889]: enabled capabilities for version 3.3Jul 23 11:27:55 k8s-m1 etcd[59889]: added member 1e713be314744d53 [https://10.252.146.105:2380] to cluster 367e2aebc6430cbe from storeJul 23 11:27:55 k8s-m1 etcd[59889]: added member 8b1621b475555fd9 [https://10.252.146.106:2380] to cluster 367e2aebc6430cbe from storeJul 23 11:27:55 k8s-m1 etcd[59889]: added member ac2dcf6aed12e8f1 [https://10.252.146.104:2380] to cluster 367e2aebc6430cbe from store 查看集群状态 123456789[root@k8s-m1 Kubernetes-ansible]# etcd-ha+-----------------------------+------------------+---------+---------+-----------+-----------+------------+| ENDPOINT | ID | VERSION | DB SIZE | IS LEADER | RAFT TERM | RAFT INDEX |+-----------------------------+------------------+---------+---------+-----------+-----------+------------+| https://10.252.146.104:2379 | ac2dcf6aed12e8f1 | 3.3.20 | 8.3 MB | false | 47 | 5953557 || https://10.252.146.105:2379 | 1e713be314744d53 | 3.3.20 | 8.6 MB | false | 47 | 5953557 || https://10.252.146.106:2379 | 8b1621b475555fd9 | 3.3.20 | 8.3 MB | true | 47 | 5953557 |+-----------------------------+------------------+---------+---------+-----------+-----------+------------+ 然后给kube-apiserver三个组件和kubelet起来后 12345[root@k8s-m1 Kubernetes-ansible]# kubectl get node -o wideNAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME10.252.146.104 Ready <none> 30d v1.16.9 10.252.146.104 <none> CentOS Linux 8 (Core) 4.18.0-193.6.3.el8_2.x86_64 docker://19.3.1110.252.146.105 Ready <none> 30d v1.16.9 10.252.146.105 <none> CentOS Linux 8 (Core) 4.18.0-193.6.3.el8_2.x86_64 docker://19.3.1110.252.146.106 Ready <none> 30d v1.16.9 10.252.146.106 <none> CentOS Linux 8 (Core) 4.18.0-193.6.3.el8_2.x86_64 docker://19.3.11 pod也在慢慢自愈了 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注 特别声明 原文作者:Zhangguanzhang 本文原链:http://zhangguanzhang.github.io/2020/07/23/fs-error-fix-k8s-master/ 本文转载如有侵权,请联系站长删除,谢谢","tags":[{"name":"K8s","slug":"K8s","permalink":"http://damon008.github.io/tags/K8s/"}]},{"title":"Spring Cloud Kubernetes之实战服务注册与发现","date":"2020-07-23T01:38:22.000Z","path":"2020/07/23/spring-cloud-k8s-discovery/","text":"好久没写文章了,本文主讲利用 k8s 来实现服务的注册与发现,甚至负载均衡,简称 LB,完美无坑版! 环境: ubuntu16.04 docker18.04 k8s1.13.x + maven3.5.3 java1.8 + springboot 2.1.1 spring-cloud-kubernetes:1.0.1.RELEASE Relax 1. 前提 Ubuntu下安装docker18.04 or 其它较高版本,k8s1.13.x及以上,jvm环境等。 2. 创建项目 我们都知道,涉及到微服务,那必体现六个字,”高内聚,低耦合”,所以针对不同业务或应用场景,服务模块化很重要,这个不再赘述了。咱们先来创建服务提供方,同样,利用eclipse或IDEA创建一个项目,此处略了。 创建好项目之后,首先引入依赖: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-actuator-autoconfigure</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-kubernetes-config</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency> 其他数据库,中间件等,可根据项目自行添加。 同样,我们需要配置初始化bean,这就涉及到配置文件bootstrap.yaml: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071# we enable some of the management endpoints to make it possible to restart the applicationmanagement: endpoint: restart: enabled: true health: enabled: true info: enabled: truespring: application: name: edge-cas cloud: kubernetes: reload: #自动更新配置的开关设置为打开 enabled: true #更新配置信息的模式:polling是主动拉取,event是事件通知 mode: event #主动拉取的间隔时间是500毫秒 #period: 500 config: sources: - name: ${spring.application.name} namespace: default discovery: all-namespaces: true http: encoding: charset: UTF-8 enabled: true force: true mvc: throw-exception-if-no-handler-found: true main: allow-bean-definition-overriding: true # 当遇到同样名称时,是否允许覆盖注册 接下来就是application.yaml: 12345678910111213141516171819202122232425262728293031323334353637383940server: port: 1000 undertow: accesslog: enabled: false pattern: combined servlet: session: timeout: PT120Mlogging: path: /data/${spring.application.name}/logsclient: http: request: connectTimeout: 8000 readTimeout: 30000 mybatis: mapperLocations: classpath:mapper/*.xml typeAliasesPackage: com.gemantic.*.model 到这,基本的配置即完成,同样,我们也引入了k8s的configmap功能,可以新建configmap的yaml文件来创建其configmap。 然后最重要的一点,就是我们需要创建service: 1234567891011121314151617181920212223apiVersion: v1kind: Servicemetadata: name: demo-cas-service namespace: defaultspec: ports: - name: cas01 port: 1000 targetPort: cas01 selector: app: demo-cas 这一点很关键,即实现了服务的注册。 然后服务提供者的项目架子搭建好了,自己可以添加一些内容,比如我把它作为微服务架构的统一鉴权中心CAS。 接下来创建服务消费者的项目,同样引入依赖,但这一次不同: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-actuator-autoconfigure</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-kubernetes-config</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>io.kubernetes</groupId><artifactId>client-java</artifactId><version>${kubernetes-client-version}</version><exclusions><exclusion><groupId>com.squareup.okio</groupId> <artifactId>okio</artifactId></exclusion></exclusions></dependency><!-- springcloud-k8s-discovery --><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-commons</artifactId> </dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-kubernetes-core</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-kubernetes-discovery</artifactId> </dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-kubernetes-ribbon</artifactId> </dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> 以上是服务消费者的必须依赖,其他的可根据项目自行添加,比如:在线文档swagger,数据库,json解析,权限管理shiro等。 同样,我们也需要配置初始化bean,这就涉及到配置文件bootstrap.yaml:如上 接下来需要配置服务消费者的消费逻辑以及实现负载均衡的策略(application.yaml): 123456789101112131415161718192021222324252627282930313233343536373839404142server: port: 1002 undertow: accesslog: enabled: false pattern: combined servlet: session: timeout: PT120M logging: path: /data/${spring.application.name}/logsclient: http: request: connectTimeout: 8000 readTimeout: 30000 mybatis: mapperLocations: classpath:mapper/*.xml typeAliasesPackage: com.gemantic.*.model #这是针对所有的提供者服务的消费策略: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869backend: ribbon: eureka: enabled: false client: enabled: true ServerListRefreshInterval: 5000 ribbon: ConnectTimeout: 3000 # 设置全局默认的ribbon的读超时 ReadTimeout: 1000 eager-load: enabled: true clients: demo-cas-service,cloud-admin-service MaxAutoRetries: 1 #对第一次请求的服务的重试次数 MaxAutoRetriesNextServer: 1 #要重试的下一个服务的最大数量(不包括第一个服务) #listOfServers: localhost:5556,localhost:5557 #ServerListRefreshInterval: 2000 OkToRetryOnAllOperations: true NFLoadBalancerRuleClassName:com.netflix.loadbalancer.RoundRobinRule#这个是针对某个指定服务来进行配置负载均衡的策略#demo-cas-service:# ribbon:# ConnectTimeout: 3000# ReadTimeout: 60000# MaxAutoRetries: 1 #对第一次请求的服务的重试次数# MaxAutoRetriesNextServer: 1 #要重试的下一个服务的最大数量(不包括第一个服务)# listOfServers: localhost:5556,localhost:5557# ServerListRefreshInterval: 2000# OkToRetryOnAllOperations: true#NFLoadBalancerRuleClassName:com.netflix.loadbalancer.RoundRobinRulehystrix.command.BackendCall.execution.isolation.thread.timeoutInMilliseconds: 5000hystrix.threadpool.BackendCallThread.coreSize: 5 这样,服务提供者与服务消费者就都新建成功了,接下来就需要丰满自己的业务应用逻辑了,同样,消费者也可以创建configmap来配置管理自己的配置。 接下来就是亲测: 这里,消费者调用提供者,提供者是个cas服务,则: 1234567891011121314151617181920212223242526272829MultiValueMap<String, String> map = new LinkedMultiValueMap<>();map.add(\"username\", username);map.add(\"password\", password);logger.info(\"CAS URL: {}\", envConfig.getCas_url());String respBody = HttpRequestUtil.doPostForm(restTemplate, envConfig.getCas_url(), map);if (StringUtils.isNotBlank(respBody)) {JSONObject pobj = JSON.parseObject(respBody);Object object = pobj.get(\"message\");Integer code = JSON.parseObject(object.toString()).getInteger(\"code\");if (code == LoginEnum.LOGIN_SUCCESS.getSeq()) {Object data = pobj.get(\"data\");SysUserDto sysUser = JSON.parseObject(data.toString(), SysUserDto.class);return sysUser;}} 这里的环境变量即使configmap提供,值:cas_url: http://demo-cas-service/login,这样我们就完成了调用的逻辑。 亲测有效: 接下来我们如果需要测试LB,需要添加一条脚本: 增加pod: 1kubectl scale --replicas=2 deployment demo-cas-deployment 这样,我们既看到两个demo-cas-deployment的pod: 同样测试,根据策略轮询调用的方式,这次会请求到该pod上,这里不贴截图了,大家可以试试。 以上,即是分享了k8s带来的第二大优点: 通过service的方式提供了服务的注册与发现,而且单机的k8s本身也不重,所以操作起来也非常之简单。避免了springboot原生提供的eureka、阿里的nacos、zk来作分布式的服务注册与发现要简单的多。减轻系统的繁重,以及避免了系统的冗余。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"后端","slug":"后端","permalink":"http://damon008.github.io/tags/%E5%90%8E%E7%AB%AF/"},{"name":"Java","slug":"Java","permalink":"http://damon008.github.io/tags/Java/"},{"name":"K8s","slug":"K8s","permalink":"http://damon008.github.io/tags/K8s/"}]},{"title":"k8s部署手册","date":"2020-07-23T01:06:27.000Z","path":"2020/07/23/k8s/","text":"1、 K8S 的由来K8S 是 kubernetes 的英文缩写,是用 8 代替 8 个字符 “ubernete” 而成的缩写。 2、 K8S 单机版实战环境: ubuntu 16.04 gpu 驱动 418.56 docker 18.06 k8s 1.13.5 一、设置环境首先备份一下源配置: 1cp /etc/apt/sources.list /etc/apt/sources.list.cp 编辑,替换为阿里源: 12345678910111213141516171819vim /etc/apt/sources.listdeb-src http://archive.ubuntu.com/ubuntu xenial main restricteddeb http://mirrors.aliyun.com/ubuntu/ xenial main restricteddeb-src http://mirrors.aliyun.com/ubuntu/ xenial main restricted multiverse universedeb http://mirrors.aliyun.com/ubuntu/ xenial-updates main restricteddeb-src http://mirrors.aliyun.com/ubuntu/ xenial-updates main restricted multiverse universedeb http://mirrors.aliyun.com/ubuntu/ xenial universedeb http://mirrors.aliyun.com/ubuntu/ xenial-updates universedeb http://mirrors.aliyun.com/ubuntu/ xenial multiversedeb http://mirrors.aliyun.com/ubuntu/ xenial-updates multiversedeb http://mirrors.aliyun.com/ubuntu/ xenial-backports main restricted universe multiversedeb-src http://mirrors.aliyun.com/ubuntu/xenial-backports main restricted universe multiversedeb http://archive.canonical.com/ubuntu xenial partnerdeb-src http://archive.canonical.com/ubuntu xenial partnerdeb http://mirrors.aliyun.com/ubuntu/ xenial-security main restricteddeb-src http://mirrors.aliyun.com/ubuntu/ xenial-security main restricted multiverse universedeb http://mirrors.aliyun.com/ubuntu/ xenial-security universedeb http://mirrors.aliyun.com/ubuntu/ xenial-security multiverse 更新源: 1apt-get update 自动修复安装出现 broken 的 package: 1apt --fix-broken install 升级,对于 gpu 机器可不执行,否则可能升级 gpu 驱动导致问题: 1apt-get upgrade 关闭防火墙: 1ufw disable 安装 selinux: 1apt install selinux-utils selinux 防火墙配置: 12345setenforce 0vim/etc/selinux/conifgSELINUX=disabled 设置网络: 1234567tee /etc/sysctl.d/k8s.conf <<-'EOF'net.bridge.bridge-nf-call-ip6tables = 1net.bridge.bridge-nf-call-iptables = 1net.ipv4.ip_forward = 1EOFmodprobe br_netfilter 查看 ipv4 与 v6 配置是否生效: 1sysctl --system 配置 iptables: 1234iptables -P FORWARD ACCEPTvim /etc/rc.local/usr/sbin/iptables -P FORWARD ACCEPT 永久关闭 swap 分区: 1sed -i 's/.*swap.*/#&/' /etc/fstab 二、安装 docker执行下面的命令: 1234567891011apt-get install apt-transport-https ca-certificates curl software-properties-commoncurl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | apt-key add -add-apt-repository \"deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\" apt-get updateapt-get purge docker-ce docker docker-engine docker.io && rm -rf /var/lib/dockerapt-get autoremove docker-ce docker docker-engine docker.ioapt-get install -y docker-ce=18.06.3~ce~3-0~ubuntu 启动 docker 并设置开机自重启: 1systemctl enable docker && systemctl start docker Docker 配置: 1234567891011121314151617vim /etc/docker/daemon.json{ \"log-driver\": \"json-file\", \"log-opts\": { \"max-size\": \"100m\", \"max-file\": \"10\" }, \"insecure-registries\": [\"http://k8s.gcr.io\"], \"data-root\": \"\", \"default-runtime\": \"nvidia\", \"runtimes\": { \"nvidia\": { \"path\": \"/usr/bin/nvidia-container-runtime\", \"runtimeArgs\": [] } }} 上面是含 GPU 的配置,不含 GPU 的配置: 123456789101112131415{\"registry-mirrors\":[\"https://registry.docker-cn.com\"],\"storage-driver\":\"overlay2\",\"log-driver\":\"json-file\",\"log-opts\":{\"max-size\":\"100m\"},\"exec-opts\":[\"native.cgroupdriver=systemd\"],\"insecure-registries\":[\"http://k8s.gcr.io\"],\"live-restore\":true} 重启服务并设置开机自动重启: 1systemctl daemon-reload && systemctl restart docker && docker info 三、安装 k8s拉取镜像前的设置: 1234567apt-get update && apt-get install -y apt-transport-https curlcurl -s https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | apt-key add -tee /etc/apt/sources.list.d/kubernetes.list <<-'EOF'deb https://mirrors.aliyun.com/kubernetes/apt kubernetes-xenial mainEOF 更新: 12345apt-get updateapt-get purge kubelet=1.13.5-00 kubeadm=1.13.5-00 kubectl=1.13.5-00apt-get autoremove kubelet=1.13.5-00 kubeadm=1.13.5-00 kubectl=1.13.5-00apt-get install -y kubelet=1.13.5-00 kubeadm=1.13.5-00 kubectl=1.13.5-00apt-mark hold kubelet=1.13.5-00 kubeadm=1.13.5-00 kubectl=1.13.5-00 启动服务并设置开机自动重启: 1systemctl enable kubelet && sudo systemctl start kubelet 安装 k8s 相关镜像,由于 gcr.io 网络访问不了,从 registry.cn-hangzhou.aliyuncs.com 镜像地址下载: 12345678910111213docker pull registry.cn-hangzhou.aliyuncs.com/gg-gcr-io/kube-apiserver:v1.13.5docker pull registry.cn-hangzhou.aliyuncs.com/gg-gcr-io/kube-controller-manager:v1.13.5docker pull registry.cn-hangzhou.aliyuncs.com/gg-gcr-io/kube-scheduler:v1.13.5docker pull registry.cn-hangzhou.aliyuncs.com/gg-gcr-io/kube-proxy:v1.13.5docker pull registry.cn-hangzhou.aliyuncs.com/kuberimages/pause:3.1docker pull registry.cn-hangzhou.aliyuncs.com/kuberimages/etcd:3.2.24docker pull registry.cn-hangzhou.aliyuncs.com/kuberimages/coredns:1.2.6 打标签: 12345678910111213docker tag registry.cn-hangzhou.aliyuncs.com/gg-gcr-io/kube-apiserver:v1.13.5 k8s.gcr.io/kube-apiserver:v1.13.5docker tag registry.cn-hangzhou.aliyuncs.com/gg-gcr-io/kube-controller-manager:v1.13.5 k8s.gcr.io/kube-controller-manager:v1.13.5docker tag registry.cn-hangzhou.aliyuncs.com/gg-gcr-io/kube-scheduler:v1.13.5 k8s.gcr.io/kube-scheduler:v1.13.5docker tag registry.cn-hangzhou.aliyuncs.com/gg-gcr-io/kube-proxy:v1.13.5 k8s.gcr.io/kube-proxy:v1.13.5docker tag registry.cn-hangzhou.aliyuncs.com/kuberimages/pause:3.1 k8s.gcr.io/pause:3.1docker tag registry.cn-hangzhou.aliyuncs.com/kuberimages/etcd:3.2.24 k8s.gcr.io/etcd:3.2.24docker tag registry.cn-hangzhou.aliyuncs.com/kuberimages/coredns:1.2.6 k8s.gcr.io/coredns:1.2.6 四、kubeadm 初始化利用 kubeadm 初始化 k8s,其中主机 IP 根据自己的实际情况输入: 1kubeadm init --kubernetes-version=v1.13.5 --pod-network-cidr=10.244.0.0/16 --service-cidr=10.16.0.0/16 --apiserver-advertise-address=${masterIp} | tee kubeadm-init.log 此时,如果未知主机 IP,也可利用 yaml 文件动态初始化: 12345678910111213141516vi /etc/hosts10.10.5.100 k8s.api.servervi kube-init.yamlapiVersion: kubeadm.k8s.io/v1beta1kind: ClusterConfigurationkubernetesVersion: v1.13.5imageRepository: registry.aliyuncs.com/google_containersapiServer: certSANs: - \"k8s.api.server\"controlPlaneEndpoint: \"k8s.api.server:6443\"networking: serviceSubnet: \"10.1.0.0/16\" podSubnet: \"10.244.0.0/16\" HA 版本: 1234567891011121314151617apiVersion: kubeadm.k8s.io/v1beta1kind: ClusterConfigurationkubernetesVersion: v1.13.5imageRepository: registry.aliyuncs.com/google_containersapiServer: certSANs: - \"api.k8s.com\"controlPlaneEndpoint: \"api.k8s.com:6443\"etcd: external: endpoints: - https://ETCD_0_IP:2379 - https://ETCD_1_IP:2379 - https://ETCD_2_IP:2379networking: serviceSubnet: 10.1.0.0/16 podSubnet: 10.244.0.0/16 注意: apiVersion 中用 kubeadm,因为需要用 kubeadm 来初始化,最后执行下面来初始化: 1kubeadm init --config=kube-init.yaml 出现问题,解决后,reset 后再执行,如果需要更多,执行: 1kubeadm --help 五、部署出现问题先删除 node 节点(集群版) 123kubectl drain <node name> --delete-local-data --force --ignore-daemonsetskubectl delete node <node name> 清空 init 配置在需要删除的节点上执行(注意,当执行 init 或者 join 后出现任何错误,都可以使用此命令返回): 1kubeadm reset 六、查问题初始化后出现问题,可以通过以下命令先查看其容器状态以及网络情况: 12345678910111213141516171819sudo docker ps -a | grep kube | grep -v pausesudo docker logs CONTAINERIDsudo docker images && systemctl status -l kubeletnetstat -nlptkubectl describe ep kuberneteskubectl describe svc kuberneteskubectl get svc kuberneteskubectl get epnetstat -nlpt | grep apiservi /var/log/syslog 七、给当前用户配置 k8s apiserver 访问公钥12345sudo mkdir -p $HOME/.kubesudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/configsudo chown $(id -u):$(id -g) $HOME/.kube/config 八、网络插件123456789101112kubectl apply -f https://docs.projectcalico.org/v3.3/getting-started/kubernetes/installation/hosted/rbac-kdd.yamlwget https://docs.projectcalico.org/v3.3/getting-started/kubernetes/installation/hosted/kubernetes-datastore/calico-networking/1.7/calico.yamlvi calico.yaml- name: CALICO_IPV4POOL_IPIP value:\"off\"- name: CALICO_IPV4POOL_CIDR value: \"10.244.0.0/16kubectl apply -f calico.yaml 单机下允许 master 节点部署 pod 命令如下: 1kubectl taint nodes --all node-role.kubernetes.io/master- 禁止 master 部署 pod: 1kubectl taint nodes k8s node-role.kubernetes.io/master=true:NoSchedule 以上单机版部署结束,如果你的项目中,交付的是软硬件结合的一体机,那么到此就结束了。记得单机下要允许 master 节点部署哟! 接下来,集群版本上线咯!以上面部署的机器为例,作为 master 节点,继续执行: 123456789scp /etc/kubernetes/admin.conf $nodeUser@$nodeIp:/home/$nodeUserscp /etc/kubernetes/pki/etcd/* $nodeUser@$nodeIp:/home/$nodeUser/etcdkubeadm token generatekubeadm token create $token_name --print-join-command --ttl=0kubeadm join $masterIP:6443 --token $token_name --discovery-token-ca-cert-hash $hash Node 机器执行时,如果需要 cuda ,可以参考以下资料: 123https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html#ubuntu-installationhttps://blog.csdn.net/u012235003/article/details/54575758https://blog.csdn.net/qq_39670011/article/details/90404111 正式执行: 1234vim /etc/modprobe.d/blacklist-nouveau.confblacklist nouveauoptions nouveau modeset=0update-initramfs -u 重启 ubuntu 查看是否禁用成功: 1234567lsmod | grep nouveauapt-get remove --purge nvidia*https://developer.nvidia.com/cuda-downloadssudo apt-get install freeglut3-dev build-essential libx11-dev libxmu-dev libxi-dev libgl1-mesa-glx libglu1-mesa libglu1-mesa-dev 安装 cuda: 123456789acceptselect \"Install\" / Enterselect \"Yes\"sh cuda_10.1.168_418.67_linux.runecho 'export PATH=/usr/local/cuda-10.1/bin:$PATH' >> ~/.bashrcecho 'export PATH=/usr/local/cuda-10.1/NsightCompute-2019.3:$PATH' >> ~/.bashrcecho 'export LD_LIBRARY_PATH=/usr/local/cuda-10.1/lib64:$LD_LIBRARY_PATH' >> ~/.bashrcsource ~/.bashrc 重启机器,检查 cuda 是否安装成功。 查看是否有“nvidia*”的设备: 1cd /dev && ls -al 如果没有,创建一个 nv.sh: 123456789101112131415161718192021222324252627282930313233343536vi nv.sh#!/bin/bash /sbin/modprobe nvidiaif [ \"$?\" -eq 0 ];thenNVDEVS=`lspci | grep -i NVIDIA`N3D=`echo\"$NVDEVS\"| grep \"3D controller\" | wc -l`NVGA=`echo\"$NVDEVS\"| grep \"VGA compatible controller\" | wc -l`N=`expr $N3D + $NVGA -1`for i in `seq0 $N`; do mknod -m 666 /dev/nvidia$i c 195 $idone mknod -m 666 /dev/nvidiactl c 195 255else exit 1fichmod +x nv.sh && bash nv.sh 再次重启机器查看 cuda 版本: 1nvcc -V 编译: 123cd /usr/local/cuda-10.1/samples && makecd /usr/local/cuda-10.1/samples/bin/x86_64/linux/release ./deviceQuery 以上如果输出:“Result = PASS” 代表 cuda 安装成功。 安装 nvdocker: 12345678910111213141516171819vim /etc/docker/daemon.json{\"runtimes\":{ \"nvidia\":{ \"path\":\"nvidia-container-runtime\", \"runtimeArgs\":[] }},\"registry-mirrors\":[\"https://registry.docker-cn.com\"],\"storage-driver\":\"overlay2\",\"default-runtime\":\"nvidia\",\"log-driver\":\"json-file\",\"log-opts\":{ \"max-size\":\"100m\"},\"exec-opts\": [\"native.cgroupdriver=systemd\"],\"insecure-registries\": [$harborRgistry],\"live-restore\": true} 重启 docker: 1sudo systemctl daemon-reload && sudo systemctl restart docker && docker info 检查 nvidia-docker 安装是否成功: 1docker run --runtime=nvidia --rm nvidia/cuda:9.0-base nvidia-smi 在节点机器进入 su 模式: 1su $nodeUser 给当前节点用户配置 k8s apiserver 访问公钥: 123456789101112131415mkdir -p $HOME/.kubecp -i admin.conf $HOME/.kube/configchown $(id -u):$(id -g) $HOME/.kube/configmkdir -p $HOME/etcdsudo rm -rf /etc/kubernetessudo mkdir -p /etc/kubernetes/pki/etcdsudo cp /home/$nodeUser/etcd/* /etc/kubernetes/pki/etcdsudo kubeadm join $masterIP:6443 --token $token_name --discovery-token-ca-cert-hash $hash 如: 1sudo kubeadm join 192.168.8.116:6443 --token vyi4ga.foyxqr2iz9i391q3 --discovery-token-ca-cert-hash sha256:929143bcdaa3e23c6faf20bc51ef6a57df02edf9df86cedf200320a9b4d3220a 检查 node 是否加入 master: 1kubectl get node 以上介绍了单机的 k8s 部署,以及 HA 的 master 节点的部署安装。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"K8s","slug":"K8s","permalink":"http://damon008.github.io/tags/K8s/"}]},{"title":"浅入Spring Cloud架构","date":"2020-07-23T01:00:09.000Z","path":"2020/07/23/micro-service03/","text":"1、 微服务简介 1.1 什么是微服务&emsp;&emsp;所谓微服务,就是把一个比较大的单个应用程序或服务拆分为若干个独立的、粒度很小的服务或组件。 1.2 为什么使用微服务&emsp;&emsp;微服务的拆解业务,这一策略,可扩展单个组件,而不需要整个的应用程序堆栈做修改,从而满足服务等级协议。微服务带来的好处是,它们更快且更容易更新。当开发者对一个传统的单体应用程序进行变更时,他们必须做详细、完整的 QA 测试,以确保变更不会影响其他特性或功能。但有了微服务,开发者可以更新应用程序的单个组件,而不会影响其他的部分。测试微服务应用程序仍然是必需的,但使得其更容易被识别和隔离,从而加快开发速度并支持 DevOps 和持续应用程序开发。 1.3 微服务的架构组成&emsp;&emsp;这几年的快速发展,微服务已经变得越来越流行。其中,Spring Cloud 一直在更新,并被大部分公司所使用。代表性的有 Alibaba,2018 年 11 月左右,Spring Cloud 联合创始人 Spencer Gibb 在 Spring 官网的博客页面宣布:阿里巴巴开源 Spring Cloud Alibaba,并发布了首个预览版本。随后,Spring Cloud 官方 Twitter 也发布了此消息。Spring Cloud 的版本也很多: Spring Cloud Spring Cloud Alibaba Spring Boot Spring Cloud Hoxton 2.2.0.RELEASE 2.2.X.RELEASE Spring Cloud Greenwich 2.1.1.RELEASE 2.1.X.RELEASE Spring Cloud Finchley 2.0.1.RELEASE 2.0.X.RELEASE Spring Cloud Edgware 1.5.1.RELEASE 1.5.X.RELEASE 以 Spring Boot1.x 为例,主要包括 Eureka、Zuul、Config、Ribbon、Hystrix 等。而在 Spring Boot2.x 中,网关采用了自己的 Gateway。当然在 Alibaba 版本中,其组件更是丰富:使用 Alibaba 的 Nacos 作为注册中心和配置中心。使用自带组件 Sentinel 作为限流、熔断神器。 2、 微服务之网关 2.1 常见的几种网关&emsp;&emsp;目前,在 Spring Boot1.x 中,用到的比较多的网关就是 Zuul。Zuul 是 Netflix 公司开源的一个网关服务,而 Spring Boot2.x 中,采用了自家推出的 Spring Cloud Gateway。 2.2 API 网关的作用&emsp;&emsp;API 网关的主要作用是反向路由、安全认证、负载均衡、限流熔断、日志监控。在 Zuul 中,我们可以通过注入 Bean 的方式来配置路由,也可以在直接通过配置文件来配置: 123zuul.routes.api-d.sensitiveHeaders=\"*\"zuul.routes.api-d.path=/business/api/**zuul.routes.api-d.serviceId=business-web 我们可以通过网关来做一些安全的认证:如统一鉴权。在 Zuul 中: Zuul 的工作原理 过滤器机制 &emsp;&emsp;zuul 的核心是一系列的 filters, 其作用可以类比 Servlet 框架的 Filter,或者 AOP。zuul 把 Request route 到用户处理逻辑的过程中,这些 filter 参与一些过滤处理,比如 Authentication,Load Shedding 等。几种标准的过滤器类型: &emsp;&emsp;(1) PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。 &emsp;&emsp;(2) ROUTING:这种过滤器用于构建发送给微服务的请求,并使用 Apache HttpClient 或 Netfilx Ribbon 请求微服务。 &emsp;&emsp;(3) POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。 &emsp;&emsp;(4) ERROR:在其他阶段发生错误时执行该过滤器。 过滤器的生命周期 &emsp;&emsp;filterOrder:通过 int 值来定义过滤器的执行顺序,越小优先级越高。 &emsp;&emsp;shouldFilter:返回一个 boolean 类型来判断该过滤器是否要执行,所以通过此函数可实现过滤器的开关。在上例中,我们直接返回 true,所以该过滤器总是生效。 &emsp;&emsp;run:过滤器的具体逻辑。需要注意,这里我们通过 ctx.setSendZuulResponse(false) 令 zuul 过滤该请求,不对其进行路由,然后通过 ctx.setResponseStatusCode(401) 设置了其返回的错误码。 代码示例: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384@Componentpublic class AccessFilter extends ZuulFilter { private static Logger logger = LoggerFactory.getLogger(AccessFilter.class); @Autowired RedisCacheConfiguration redisCacheConfiguration; @Autowired EnvironmentConfig env; private static final String[] PASS_PATH_ARRAY = { \"/login\", \"openProject\" }; @Override public String filterType() { return \"pre\"; } @Override public int filterOrder() { return 0; } @Override public boolean shouldFilter() { return true; } @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); HttpServletResponse response = ctx.getResponse(); response.setCharacterEncoding(\"UTF-8\"); response.setHeader(\"content-type\", \"text/html;charset=UTF-8\"); logger.info(\"{} request to {}\", request.getMethod(), request.getRequestURL()); for (String path : PASS_PATH_ARRAY) { if (StringUtils.contains(request.getRequestURL().toString(), path)) { logger.debug(\"request path: {} is pass\", path); return null; } } String token = request.getHeader(\"token\"); if (StringUtils.isEmpty(token)) { logger.warn(\"access token is empty\"); ctx.setSendZuulResponse(false); ctx.setResponseStatusCode(404); ctx.setResponseBody(JSONObject.toJSONString( Response.error(200, -3, \"header param error\", null))); return ctx; } Jedis jedis = null; try { JedisPool jedisPool = redisCacheConfiguration.getJedisPool(); jedis = jedisPool.getResource(); logger.debug(\"zuul gateway service get redisResource success\"); String key = env.getPrefix() + token; String value = jedis.get(key); if (StringUtils.isBlank(value)) { ctx.setSendZuulResponse(false); ctx.setResponseStatusCode(401); ctx.setResponseBody(JSONObject.toJSONString(Response.error(200, -1, \"login timeout\",null))); return ctx; } else { logger.debug(\"access token ok\"); return null; } } catch (Exception e) { logger.error(\"get redisResource failed\"); logger.error(e.getMessage(), e); ctx.setSendZuulResponse(false); ctx.setResponseStatusCode(500); ctx.setResponseBody(JSONObject.toJSONString( Response.error(200, -8, \"redis connect failed\", null))); return ctx; } finally { if (jedis != null) { jedis.close(); } } }} 3、 微服务之服务注册与发现 3.1 常见的几种注册中心&emsp;&emsp;目前常见的几种注册中心有:Eureka、Consul、Nacos,但其实 Kubernetes 也可以实现服务的注册与发现功能,且听下面讲解。 Eureka 的高可用 在注册中心部署时,有可能出现节点问题,我们先看看 Eureka 集群如何实现高可用,首先配置基础的 Eureka 配置: 1234567891011121314151617181920212223242526spring.application.name=eureka-serverserver.port=1111spring.profiles.active=deveureka.instance.hostname=localhosteureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/logging.path=/data/${spring.application.name}/logseureka.server.enable-self-preservation=falseeureka.client.register-with-eureka=falseeureka.client.fetch-registry=falseeureka.server.eviction-interval-timer-in-ms=5000eureka.server.responseCacheUpdateInvervalMs=60000eureka.instance.lease-expiration-duration-in-seconds=10eureka.instance.lease-renewal-interval-in-seconds=3eureka.server.responseCacheAutoExpirationInSeconds=180server.undertow.accesslog.enabled=falseserver.undertow.accesslog.pattern=combined 配置好后,新建一个 application-peer1.properties 文件: 1234spring.application.name=eureka-serverserver.port=1111eureka.instance.hostname=peer1eureka.client.serviceUrl.defaultZone=http://peer2:1112/eureka/ application-peer2.properties 文件: 1234spring.application.name=eureka-serverserver.port=1112eureka.instance.hostname=peer2eureka.client.serviceUrl.defaultZone=http://peer1:1111/eureka/ 这样通过域名 peer1、peer2 的形式来实现高可用,那么如何配置域名呢?有几种方式: 通过 hosts 来配置域名,vi /etc/hosts: 1210.12.3.2 peer110.12.3.5 peer2 通过 kubernetes 部署服务时来配置域名: 1234567hostAliases:- ip: \"10.12.3.2\" hostnames: - \"peer1\"- ip: \"10.12.3.5\" hostnames: - \"peer2\" Nacos 实现服务注册、发现 Nacos 是 Alibaba 推出来的,目前最新版本是 v1.2.1。其功能可以实现服务的注册、发现,也可以作为配置管理来提供配置服务。可以手动去官网下载安装,Nacos 地址:https://github.com/alibaba/nacos/releases。 执行,Linux/Unix/Mac: 1sh startup.sh -m standalone Windows: 1cmd startup.cmd -m standalone 当我们引入 Nacos 相关配置时,即可使用它: 123456789<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> 注意:下面这个配置文件需要是 bootstrap,否则可能失败,至于为什么,大家可以自己试试。12345678910spring: application: name: oauth-cas cloud: nacos: discovery: server-addr: 127.0.0.1:8848 config: server-addr: 127.0.0.1:8848 refreshable-dataids: actuator.properties,log.properties 配置完成后,完成 main: 1234567891011121314151617package com.damon;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.EnableAutoConfiguration;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;@Configuration@EnableAutoConfiguration@ComponentScan(basePackages = {\"com.damon\"})@EnableDiscoveryClientpublic class CasApp { public static void main(String[] args) { SpringApplication.run(CasApp.class, args); }} 完成以上,我们运行启动类,我们打开 Nacos 登录后,打开服务列表,即可看到: Kubernetes 服务注册与发现 接下来,请允许我为大家引入 Kubernetes 的服务注册与发现功能,spring-cloud-kubernetes 的 DiscoveryClient 服务将 Kubernetes 中的 \"Service\" 资源与 Spring Cloud 中的服务对应起来了,有了这个 DiscoveryClient,我们在 Kubernetes 环境下就不需要 Eureka 等来做注册发现了,而是直接使用 Kubernetes 的服务机制。 在 pom.xml 中,有对 spring-cloud-kubernetes 框架的依赖配置: 123456789<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-kubernetes-core</artifactId></dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-kubernetes-discovery</artifactId></dependency> 为何 spring-cloud-kubernetes 可以完成服务注册发现呢?首先,创建一个 Spring Boot 项目的启动类,且引入服务发现注解 @EnableDiscoveryClient,同时需要开启服务发现: 1234567spring: application: name: edge-admin cloud: kubernetes: discovery: all-namespaces: true 开启后,我们打开spring-cloud-kubernetes-discovery的源码,地址是:https://github.com/spring-cloud/spring-cloud-kubernetes/tree/master/spring-cloud-kubernetes-discovery,看到内容: 为什么要看这个文件呢?因为 spring 容器启动时,会寻找 classpath 下所有 spring.factories 文件(包括 jar 文件中的),spring.factories 中配置的所有类都会实例化,我们在开发 springboot 时常用到的***-starter.jar 就用到了这个技术,效果是一旦依赖了某个 starter.jar 很多功能就在 spring 初始化时候自动执行。 spring.factories 文件中有两个类:KubernetesDiscoveryClientAutoConfiguration 和 KubernetesDiscoveryClientConfigClientBootstrapConfiguration 都会被实例化。先看 KubernetesDiscoveryClientConfigClientBootstrapConfiguration,KubernetesAutoConfiguration 和 KubernetesDiscoveryClientAutoConfiguration 这两个类会被实例化: 12345678910111213141516 * Copyright 2013-2019 the original author or authors.package org.springframework.cloud.kubernetes.discovery;import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;import org.springframework.cloud.kubernetes.KubernetesAutoConfiguration;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Import;@Configuration@ConditionalOnProperty(\"spring.cloud.config.discovery.enabled\")@Import({ KubernetesAutoConfiguration.class, KubernetesDiscoveryClientAutoConfiguration.class })public class KubernetesDiscoveryClientConfigClientBootstrapConfiguration {} 再看 KubernetesAutoConfiguration 的源码,会实例化一个重要的类 DefaultKubernetesClient,如下: 12345@Bean@ConditionalOnMissingBeanpublic KubernetesClient kubernetesClient(Config config) { return new DefaultKubernetesClient(config);} 最后我们再看 KubernetesDiscoveryClientAutoConfiguration 源码,注意 kubernetesDiscoveryClient 方法,这里面是接口实现的重点,还要重点关注的地方是 KubernetesClient 参数的值,是上面提到的 DefaultKubernetesClient 对象: 123456789@Bean@ConditionalOnMissingBean@ConditionalOnProperty(name = \"spring.cloud.kubernetes.discovery.enabled\", matchIfMissing = true)public KubernetesDiscoveryClient kubernetesDiscoveryClient(KubernetesClient client, KubernetesDiscoveryProperties properties, KubernetesClientServicesFunction kubernetesClientServicesFunction, DefaultIsServicePortSecureResolver isServicePortSecureResolver) { return new KubernetesDiscoveryClient(client, properties, kubernetesClientServicesFunction, isServicePortSecureResolver);} 接下来,我们看 spring-cloud-kubernetes 中的 KubernetesDiscoveryClient.java,看方法: 12345public List<String> getServices(Predicate<Service> filter) { return this.kubernetesClientServicesFunction.apply(this.client).list().getItems() .stream().filter(filter).map(s -> s.getMetadata().getName()) .collect(Collectors.toList());} 在 apply(this.client).list(),可以看到数据源其实就是 this.client,并且 KubernetesClientServicesFunction 实例化时: 123456789@Beanpublic KubernetesClientServicesFunction servicesFunction( KubernetesDiscoveryProperties properties) { if (properties.getServiceLabels().isEmpty()) { return KubernetesClient::services; } return (client) -> client.services().withLabels(properties.getServiceLabels());} 调用其 services 方法的返回结果,KubernetesDiscoveryClient.getServices 方法中的 this.client 是什么呢?在前面的分析时已经提到了,就是 DefaultKubernetesClient 类的实例,所以,此时要去去看 DefaultKubernetesClient.services 方法,发现 client 是 ServiceOperationsImpl: 1234@Override public MixedOperation<Service, ServiceList, DoneableService, ServiceResource<Service, DoneableService>> services() { return new ServiceOperationsImpl(httpClient, getConfiguration(), getNamespace()); } 接着我们在实例 ServiceOperationsImpl 中看其 list 函数: 12345678910111213141516171819202122public L list() throws KubernetesClientException { try { HttpUrl.Builder requestUrlBuilder = HttpUrl.get(getNamespacedUrl()).newBuilder(); String labelQueryParam = getLabelQueryParam(); if (Utils.isNotNullOrEmpty(labelQueryParam)) { requestUrlBuilder.addQueryParameter(\"labelSelector\", labelQueryParam); } String fieldQueryString = getFieldQueryParam(); if (Utils.isNotNullOrEmpty(fieldQueryString)) { requestUrlBuilder.addQueryParameter(\"fieldSelector\", fieldQueryString); } Request.Builder requestBuilder = new Request.Builder().get().url(requestUrlBuilder.build()); L answer = handleResponse(requestBuilder, listType); updateApiVersion(answer); return answer; } catch (InterruptedException | ExecutionException | IOException e) { throw KubernetesClientException.launderThrowable(forOperationType(\"list\"), e); } } 接着展开上面代码的 handleResponse 函数,可见里面是一次 http 请求,至于请求的地址,可以展开 getNamespacedUrl() 方法,里面调用的 getRootUrl 方法如下: 12345678910public URL getRootUrl() { try { if (apiGroup != null) { return new URL(URLUtils.join(config.getMasterUrl().toString(), \"apis\", apiGroup, apiVersion)); } return new URL(URLUtils.join(config.getMasterUrl().toString(), \"api\", apiVersion)); } catch (MalformedURLException e) { throw KubernetesClientException.launderThrowable(e); } } 我们看到逻辑中,貌似了解到其结果是这样的格式: 1xxx/api/version 或 xxx/apis/xxx/version 看到这样的结果,感觉比较像访问 kubernetes 的 API Server 时用的 URL 标准格式,有关 API Server 服务的详情请参考官方文档,地址是: https://kubernetes.io/docs/reference/using-api/api-concepts/。 弄清楚以上,我们发现了其实最终是向 kubernetes 的 API Server 发起 http 请求,获取 Service 资源的数据列表。因此,我们在最后还得在 k8s 底层新建 Service 资源来让其获取: 123456789101112apiVersion: v1kind: Servicemetadata: name: admin-web-service namespace: defaultspec: ports: - name: admin-web01 port: 2001 targetPort: admin-web01 selector: app: admin-web 当然,在部署时,不管是以 Deployment 形式,还是以 DaemonSet 来部署,其最后还是 pod,如果要实现单个服务的多节点部署,可以用: 1kubectl scale --replicas=2 deployment admin-web-deployment 总结: spring-cloud-kubernetes 这个组件的服务发现目的就是获取 Kubernetes 中一个或者多个 Namespace 下的所有服务列表,且在过滤列表时候设置过滤的端口号 ,这样获取到服务列表后就能让依赖它们的 Spring Boot 或其它框架的应用完成服务发现工作,让服务能够通过 http://serviceName 这种方式进行访问。 4、 微服务之配置管理 4.1 常见的配置中心 &emsp;&emsp;目前常见的几种配置中心有:Spring Cloud Config、Apollo、Nacos,但其实 Kubernetes 组件 configMap 就可以实现服务的配置管理。并且,在 Spring Boot2.x 中,就已经引入使用了。 Nacos 配置中心 &emsp;&emsp;在上面注册中心中,我们讲到 Nacos,作为注册中心,其实也可以作为配置来管理服务的环境变量。 同样,引入其以依赖: 123456789<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> 同样,注意:下面这个配置文件需要是 bootstrap,否则可能失败。 12345678910spring: application: name: oauth-cas cloud: nacos: discovery: server-addr: 127.0.0.1:8848 config: server-addr: 127.0.0.1:8848 refreshable-dataids: actuator.properties,log.properties 启动类在上面的注册中心已经讲过了,现在看其配置类: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859package com.damon.config;import org.springframework.beans.factory.annotation.Value;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.cloud.context.config.annotation.RefreshScope;import org.springframework.context.annotation.Configuration;import org.springframework.stereotype.Component;@Component@RefreshScopepublic class EnvConfig { @Value(\"${jdbc.driverClassName:}\") private String jdbc_driverClassName; @Value(\"${jdbc.url:}\") private String jdbc_url; @Value(\"${jdbc.username:}\") private String jdbc_username; @Value(\"${jdbc.password:}\") private String jdbc_password; public String getJdbc_driverClassName() { return jdbc_driverClassName; } public void setJdbc_driverClassName(String jdbc_driverClassName) { this.jdbc_driverClassName = jdbc_driverClassName; } public String getJdbc_url() { return jdbc_url; } public void setJdbc_url(String jdbc_url) { this.jdbc_url = jdbc_url; } public String getJdbc_username() { return jdbc_username; } public void setJdbc_username(String jdbc_username) { this.jdbc_username = jdbc_username; } public String getJdbc_password() { return jdbc_password; } public void setJdbc_password(String jdbc_password) { this.jdbc_password = jdbc_password; }} 我们通过注解 @Component、@RefreshScope,来实现其配置可被获取。注意 @Value(\"${jdbc.username:}\")最后需要冒号的,否则启动后会报错的。 接下来可以配置属性值来,点击配置管理,查看配置: 如果首次打开没有配置,可以新建配置: 编辑配置: 新建完之后,可以编辑,也可以删除,这里就不操作了。 ConfigMap 作为配置管理 spring-cloud-kubernetes 在上面提供了服务发现的功能,其实它还很强大,也提供了服务的配置管理: 123456789<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-actuator-autoconfigure</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-kubernetes-config</artifactId> </dependency> 在初始化时,引入注解来自动注入: 12345678910111213141516171819202122232425package com.damon;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.EnableAutoConfiguration;import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;import org.springframework.boot.context.properties.EnableConfigurationProperties;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import com.damon.config.EnvConfig;@Configuration@EnableAutoConfiguration@ComponentScan(basePackages = {\"com.damon\"})@EnableConfigurationProperties(EnvConfig.class)@EnableDiscoveryClientpublic class AdminApp { public static void main(String[] args) { SpringApplication.run(AdminApp.class, args); }} 其中,EnvConfig 类来配置环境变量配置: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151package com.damon.config;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.context.annotation.Configuration;@Configuration@ConfigurationProperties(prefix = \"damon\")public class EnvConfig { private String message = \"This is a dummy message\"; private String spring_mq_host; private String spring_mq_port; private String spring_mq_user; private String spring_mq_pwd; private String jdbc_driverClassName = \"com.mysql.jdbc.Driver\"; private String jdbc_url = \"jdbc:mysql://localhost:3306/data_test?zeroDateTimeBehavior=convertToNull&amp;useUnicode=true&amp;characterEncoding=utf-8&amp;useSSL=false\"; private String jdbc_username = \"root\"; private String jdbc_password = \"wwww\"; private String spring_redis_host; private String spring_redis_port; private String spring_redis_pwd; private String base_path; private String chunk_size; private Long expire_time= 600000L; public String getMessage() { return this.message; } public void setMessage(String message) { this.message = message; } public String getSpring_mq_host() { return spring_mq_host; } public void setSpring_mq_host(String spring_mq_host) { this.spring_mq_host = spring_mq_host; } public String getSpring_mq_port() { return spring_mq_port; } public void setSpring_mq_port(String spring_mq_port) { this.spring_mq_port = spring_mq_port; } public String getSpring_mq_user() { return spring_mq_user; } public void setSpring_mq_user(String spring_mq_user) { this.spring_mq_user = spring_mq_user; } public String getSpring_mq_pwd() { return spring_mq_pwd; } public void setSpring_mq_pwd(String spring_mq_pwd) { this.spring_mq_pwd = spring_mq_pwd; } public String getJdbc_driverClassName() { return jdbc_driverClassName; } public void setJdbc_driverClassName(String jdbc_driverClassName) { this.jdbc_driverClassName = jdbc_driverClassName; } public String getJdbc_url() { return jdbc_url; } public void setJdbc_url(String jdbc_url) { this.jdbc_url = jdbc_url; } public String getJdbc_username() { return jdbc_username; } public void setJdbc_username(String jdbc_username) { this.jdbc_username = jdbc_username; } public String getJdbc_password() { return jdbc_password; } public void setJdbc_password(String jdbc_password) { this.jdbc_password = jdbc_password; } public String getSpring_redis_host() { return spring_redis_host; } public void setSpring_redis_host(String spring_redis_host) { this.spring_redis_host = spring_redis_host; } public String getSpring_redis_port() { return spring_redis_port; } public void setSpring_redis_port(String spring_redis_port) { this.spring_redis_port = spring_redis_port; } public String getSpring_redis_pwd() { return spring_redis_pwd; } public void setSpring_redis_pwd(String spring_redis_pwd) { this.spring_redis_pwd = spring_redis_pwd; } public String getBase_path() { return base_path; } public void setBase_path(String base_path) { this.base_path = base_path; } public String getChunk_size() { return chunk_size; } public void setChunk_size(String chunk_size) { this.chunk_size = chunk_size; } public Long getExpire_time() { return expire_time; } public void setExpire_time(Long expire_time) { this.expire_time = expire_time; }} 这样,在部署时,我们新建 ConfigMap 类型的资源,同时,会配置其属性值: 1234567891011121314151617181920212223kind: ConfigMapapiVersion: v1metadata: name: admin-webdata: application.yaml: |- damon: message: Say Hello to the World --- spring: profiles: dev damon: message: Say Hello to the Developers --- spring: profiles: test damon: message: Say Hello to the Test --- spring: profiles: prod damon: message: Say Hello to the Prod 并且结合配置,来实现动态更新: 123456789101112131415spring: application: name: admin-web cloud: kubernetes: discovery: all-namespaces: true reload: enabled: true mode: polling period: 500 config: sources: - name: ${spring.application.name} namespace: default 这里是实现自动 500ms 拉取配置,也可以通过事件触发的形式来动态获取最新配置: 123456789101112131415spring: application: name: admin-web cloud: kubernetes: config: sources: - name: ${spring.application.name} namespace: default discovery: all-namespaces: true reload: enabled: true mode: event period: 500 5、 微服务模块划分 5.1 如何划分微服务 &emsp;&emsp;微服务架构设计中,服务拆分的问题很突出,第一种,按照纵向的业务拆分,第二种,横向的功能拆分。 &emsp;&emsp;以电商业务为例,首先按照业务领域的纵向拆分,分为用户微服务、商品微服务、交易微服务、订单微服务等等。 &emsp;&emsp;思考一下: 在纵向拆分仅仅按照业务领域进行拆分是否满足所有的业务场景?结果肯定是否定的。例如用户服务分为用户注册(写)和登录(读)等。写请求的重要性总是大于读请求的,在高并发下,读写比例 10:1,甚至更高的情况下,从而导致了大量的读请求往往会直接影响写请求。为了避免大量的读对写的请求干扰,需要对服务进行读写分离,即用户注册为一个微服务,登录为另一个微服务。此时按照 API 的细粒度继续进行纵向的业务拆分。 &emsp;&emsp;在横向上,按照所请求的功能进行拆分,即对一个请求的生命周期继续进行拆分。请求从用户端发出,首先接受到请求的是网关服务(这里不考虑 nginx 代理网关分发过程),网关服务对请求进行鉴权、参数合法性检查、路由转发等。接下来业务逻辑服务对请求进行业务逻辑的编排处理。对业务数据进行存储和查询就需要数据访问服务,数据访问服务提供了基本的 CRUD 原子操作,并负责海量数据的分库分表,以及屏蔽底层存储的差异性等功能。最后是数据持久化和缓存服务,比如可以采用 MQ、Kafka、Redis Cluster 等。 &emsp;&emsp;微服务架构通过业务的纵向拆分以及功能的横向拆分,服务演化成更小的颗粒度,各服务之间相互解耦,每个服务都可以快速迭代和持续交付(CI/CD),从而在公司层面能够达到降本增效的终极目标。但是服务粒度越细,服务之间的交互就会越来越多,更多的交互会使得服务之间的治理更复杂。服务之间的治理包括服务间的发现、通信、路由、负载均衡、重试机制、限流降级、熔断、链路跟踪等。 5.2 微服务划分的粒度 &emsp;&emsp;微服务划分粒度,其最核心的六个字可能就是:“高内聚、低耦合”。高内聚:就是说每个服务处于同一个网络或网域下,而且相对于外部,整个的是一个封闭的、安全的盒子,宛如一朵玫瑰花。盒子对外的接口是不变的,盒子内部各模块之间的接口也是不变的,但是各模块内部的内容可以更改。模块只对外暴露最小限度的接口,避免强依赖关系。增删一个模块,应该只会影响有依赖关系的相关模块,无关的不应该受影响。 &emsp;&emsp;那么低耦合,这就涉及到我们业务系统的设计了。所谓低耦合:就是要每个业务模块之间的关系降低,减少冗余、重复、交叉的复杂度,模块功能划分也尽可能单一。这样,才能达到低耦合的目的。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"Java","slug":"Java","permalink":"http://damon008.github.io/tags/Java/"}]},{"title":"Spring Cloud Alibaba 实战","date":"2020-07-23T00:50:27.000Z","path":"2020/07/23/springcloud-alibaba01/","text":"2018年11月左右,Springcloud 联合创始人Spencer Gibb在Spring官网的博客页面宣布:阿里巴巴开源 Spring Cloud Alibaba,并发布了首个预览版本。随后,Spring Cloud 官方Twitter也发布了此消息。` 一、环境准备 Spring Boot: 2.1.8 Spring Cloud: Greenwich.SR3 Spring Cloud Alibaba: 0.9.0.RELEASE Maven: 3.5.4 Java 1.8 + Oauth2 (Spring Security 5.1.6 +) 二、实战 项目模块 主要分为:鉴权中心、服务提供者、服务消费者、网关 实战代码 鉴权中心,依赖pom.xml: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\"> <modelVersion>4.0.0</modelVersion> <groupId>com.damon</groupId> <artifactId>oauth-cas</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>oauth-cas</name> <url>http://maven.apache.org</url> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.8.RELEASE</version> <relativePath/> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <swagger.version>2.6.1</swagger.version> <xstream.version>1.4.7</xstream.version> <pageHelper.version>4.1.6</pageHelper.version> <fastjson.version>1.2.51</fastjson.version> <!-- <springcloud.version>2.1.8.RELEASE</springcloud.version> --> <springcloud.version>Greenwich.SR3</springcloud.version> <springcloud.kubernetes.version>1.1.1.RELEASE</springcloud.kubernetes.version> <mysql.version>5.1.46</mysql.version> <alibaba-cloud.version>2.1.1.RELEASE</alibaba-cloud.version> <springcloud.alibaba.version>0.9.0.RELEASE</springcloud.alibaba.version> </properties> <dependencyManagement> <dependencies> <!-- <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${alibaba-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${springcloud.alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${springcloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>4.6.3</version> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>19.0</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.2</version> </dependency> <!-- swagger --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>${swagger.version}</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>${swagger.version}</version> </dependency> <!--分页插件--> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>${pageHelper.version}</version> </dependency> <!-- mybatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.1.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <!-- datasource pool--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.3</version> </dependency> <!-- 对redis支持,引入的话项目缓存就支持redis了,所以必须加上redis的相关配置,否则操作相关缓存会报异常 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>${fastjson.version}</version> </dependency> <dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.11.3</version> </dependency> </dependencies> <build> <finalName>${project.artifactId}</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <jvmArguments>-Dfile.encoding=UTF-8</jvmArguments> <fork>true</fork> </configuration> </plugin> <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.7.8</version> <executions> <execution> <goals> <goal>prepare-agent</goal> <goal>report</goal> </goals> </execution> </executions> </plugin> </plugins> </build></project> 本例中,用到了 Nacos 作为注册中心、配置中心,估需要引入其依赖: 12345678910<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> Oauth2 的依赖: 1234<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId></dependency> 同时利用 redis 来处理鉴权的信息存储: 1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency> 接下来需要准备配置文件 yaml: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546management: endpoint: restart: enabled: true health: enabled: true info: enabled: truespring: application: name: oauth-cas cloud: nacos: discovery: server-addr: 127.0.0.1:8848 config: server-addr: 127.0.0.1:8848 refreshable-dataids: actuator.properties,log.properties redis: #redis相关配置 database: 8 host: 127.0.0.1 port: 6379 password: qwqwsq jedis: pool: max-active: 8 max-idle: 8 min-idle: 0 timeout: 10000ms http: encoding: charset: UTF-8 enabled: true force: true mvc: throw-exception-if-no-handler-found: true main: allow-bean-definition-overriding: true # 当遇到同样名称时,是否允许覆盖注册logging: path: /data/${spring.application.name}/logs 注意,这个配置文件需要是 bootstrap,否则可能失败,至于为什么,大家可以自己试试。 接下来就是 application: 1234567891011121314151617181920server: port: 2000 undertow: accesslog: enabled: false pattern: combined servlet: session: timeout: PT120Mclient: http: request: connectTimeout: 8000 readTimeout: 30000mybatis: mapperLocations: classpath:mapper/*.xml typeAliasesPackage: com.damon.*.model 配置完成后,完成 main: 123456789101112131415161718192021222324252627package com.damon;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.EnableAutoConfiguration;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;/** * * 配置最多的就是认证服务端,验证账号、密码,存储 token,检查 token ,刷新 token 等都是认证服务端的工作 * @author Damon * @date 2020年1月13日 下午2:29:42 * */@Configuration@EnableAutoConfiguration@ComponentScan(basePackages = {\"com.damon\"})@EnableDiscoveryClientpublic class CasApp { public static void main(String[] args) { SpringApplication.run(CasApp.class, args); }} 接下来就是配置几个 Oauth2 服务端的几个配置类:AuthorizationServerConfig、ResourceServerConfig、SecurityConfig、RedisTokenStoreConfig、MyRedisTokenStore、UserOAuth2WebResponseExceptionTranslator、AuthenticationEntryPointHandle 等。在 Springcloud Oauth2 进阶篇、Springcloud Oauth2 HA篇 等几篇中已经讲过了。对于相关代码可以关注我的公众号和我互动。 其中最重要的就是登录时的函数: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899package com.damon.login.service.impl;import java.util.List;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.apache.commons.lang3.StringUtils;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.core.authority.SimpleGrantedAuthority;import org.springframework.security.core.userdetails.User;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UsernameNotFoundException;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.security.oauth2.common.OAuth2AccessToken;import org.springframework.security.oauth2.provider.token.ConsumerTokenServices;import org.springframework.stereotype.Service;import com.damon.commons.Response;import com.damon.constant.Constant;import com.damon.constant.LoginEnum;import com.damon.exception.InnerErrorException;import com.damon.login.dao.UserMapper;import com.damon.login.model.SysUser;import com.damon.login.service.LoginService;import com.damon.utils.IpUtil;import com.google.common.collect.Lists;/** * @author wangshoufa * @date 2018年11月15日 下午12:01:53 * */@Servicepublic class LoginServiceImpl implements LoginService { Logger logger = LoggerFactory.getLogger(LoginServiceImpl.class); //private List<User> userList; @Autowired private PasswordEncoder passwordEncoder; @Autowired private UserMapper userMapper; @Autowired private HttpServletRequest req; /** * Auth * 登录认证 * 实际中从数据库获取信息 * 这里为了做演示,把用户名、密码和所属角色都写在代码里了,正式环境中,这里应该是从数据库或者其他地方根据用户名将加密后的密码及所属角色查出来的。账号 damon , * 密码123456,稍后在换取 token 的时候会用到。并且给这个用户设置 \"ROLE_ADMIN\" 角色。 * */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { logger.info(\"clientIp is: {} ,username: {}\", IpUtil.getClientIp(req), username); logger.info(\"serverIp is: {}\", IpUtil.getCurrentIp()); // 查询数据库操作 try { SysUser user = userMapper.getUserByUsername(username); if (user == null) { logger.error(\"user not exist\"); throw new UsernameNotFoundException(\"username is not exist\"); //throw new UsernameNotFoundException(\"the user is not found\"); } else { // 用户角色也应在数据库中获取,这里简化 String role = \"\"; if(user.getIsAdmin() == 1) { role = \"admin\"; } List<SimpleGrantedAuthority> authorities = Lists.newArrayList(); authorities.add(new SimpleGrantedAuthority(role)); //String password = passwordEncoder.encode(\"123456\");// 123456是密码 //return new User(username, password, authorities); // 线上环境应该通过用户名查询数据库获取加密后的密码 return new User(username, user.getPassword(), authorities); } } catch (Exception e) { logger.error(\"database collect failed\"); logger.error(e.getMessage(), e); throw new UsernameNotFoundException(e.getMessage()); } } } 函数 loadUserByUsername 需要验证数据库的密码,并且给用户授权角色。 到此,鉴权中心服务端完成。上面说的利用了 Nacos 来作为注册中心被客户端服务发现,并提供配置管理。 下载 Nacos 地址:https://github.com/alibaba/nacos/releases 版本:v1.2.1 执行: Linux/Unix/Mac:sh startup.sh -m standalone Windows:cmd startup.cmd -m standalone 启动完成之后,访问:http://127.0.0.1:8848/nacos/,可以进入Nacos的服务管理页面,具体如下: 默认用户名与密码都是nacos。 登陆后打开服务管理,可以看到注册到 Nacos 的服务列表: 可以点击配置管理,查看配置: 如果没有配置任何服务的配置,可以新建: 上面讲述了Nacos 如何作为注册中心与配置中心的,很简单吧。 接下来我们讲解服务提供者代码: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\"> <modelVersion>4.0.0</modelVersion> <groupId>com.damon</groupId> <artifactId>provider-service</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>provider-service</name> <url>http://maven.apache.org</url> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.8.RELEASE</version> <relativePath/> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <swagger.version>2.6.1</swagger.version> <xstream.version>1.4.7</xstream.version> <pageHelper.version>4.1.6</pageHelper.version> <fastjson.version>1.2.51</fastjson.version> <!-- <springcloud.version>2.1.8.RELEASE</springcloud.version> --> <springcloud.version>Greenwich.SR3</springcloud.version> <springcloud.kubernetes.version>1.1.1.RELEASE</springcloud.kubernetes.version> <mysql.version>5.1.46</mysql.version> <alibaba-cloud.version>2.1.1.RELEASE</alibaba-cloud.version> <springcloud.alibaba.version>0.9.0.RELEASE</springcloud.alibaba.version> </properties> <dependencyManagement> <dependencies> <!-- <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${alibaba-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${springcloud.alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${springcloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>${fastjson.version}</version> </dependency> <!-- swagger --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>${swagger.version}</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>${swagger.version}</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.2</version> </dependency> <!--分页插件--> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>${pageHelper.version}</version> </dependency> <!-- mybatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.1.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <!-- datasource pool--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.3</version> </dependency> <!-- 对redis支持,引入的话项目缓存就支持redis了,所以必须加上redis的相关配置,否则操作相关缓存会报异常 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>19.0</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> </dependencies> <build> <finalName>${project.artifactId}</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <jvmArguments>-Dfile.encoding=UTF-8</jvmArguments> <fork>true</fork> </configuration> </plugin> <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.7.8</version> <executions> <execution> <goals> <goal>prepare-agent</goal> <goal>report</goal> </goals> </execution> </executions> </plugin> <!-- 自动生成代码 插件 begin --> <!-- <plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.2</version> <configuration> <configurationFile>src/main/resources/generatorConfig.xml</configurationFile> <verbose>true</verbose> <overwrite>true</overwrite> </configuration> <dependencies> <dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.3.2</version> </dependency> </dependencies> </plugin> --> </plugins> </build></project> 一如既往的引入依赖。 配置 bootstrap 文件; 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113management: endpoint: restart: enabled: true health: enabled: true info: enabled: truespring: application: name: provider-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848 config: server-addr: 127.0.0.1:8848 refreshable-dataids: actuator.properties,log.properties http: encoding: charset: UTF-8 enabled: true force: true mvc: throw-exception-if-no-handler-found: true main: allow-bean-definition-overriding: true #当遇到同样名称时,是否允许覆盖注册logging: path: /data/${spring.application.name}/logscas-server-url: http://oauth-cas #http://localhost:2000#设置可以访问的地址security: oauth2: #与cas对应的配置 client: client-id: provider-service client-secret: provider-service-123 user-authorization-uri: ${cas-server-url}/oauth/authorize #是授权码认证方式需要的 access-token-uri: ${cas-server-url}/oauth/token #是密码模式需要用到的获取 token 的接口 resource: loadBalanced: true #jwt: #jwt存储token时开启 #key-uri: ${cas-server-url}/oauth/token_key #key-value: test_jwt_sign_key id: provider-service #指定用户信息地址 user-info-uri: ${cas-server-url}/api/user #指定user info的URI,原生地址后缀为/auth/user prefer-token-info: false #token-info-uri: authorization: check-token-access: ${cas-server-url}/oauth/check_token #当此web服务端接收到来自UI客户端的请求后,需要拿着请求中的 token 到认证服务端做 token 验证,就是请求的这个接口application 文件;server: port: 2001 undertow: accesslog: enabled: false pattern: combined servlet: session: timeout: PT120M cookie: name: PROVIDER-SERVICE-SESSIONID #防止Cookie冲突,冲突会导致登录验证不通过client: http: request: connectTimeout: 8000 readTimeout: 30000mybatis: mapperLocations: classpath:mapper/*.xml typeAliasesPackage: com.damon.*.modelbackend: ribbon: client: enabled: true ServerListRefreshInterval: 5000ribbon: ConnectTimeout: 3000 # 设置全局默认的ribbon的读超时 ReadTimeout: 1000 eager-load: enabled: true clients: oauth-cas,consumer-service MaxAutoRetries: 1 #对第一次请求的服务的重试次数 MaxAutoRetriesNextServer: 1 #要重试的下一个服务的最大数量(不包括第一个服务) #listOfServers: localhost:5556,localhost:5557 #ServerListRefreshInterval: 2000 OkToRetryOnAllOperations: true NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRulehystrix.command.BackendCall.execution.isolation.thread.timeoutInMilliseconds: 5000hystrix.threadpool.BackendCallThread.coreSize: 5 接下来启动类: 123456789101112131415161718192021222324252627282930313233package com.damon;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.EnableAutoConfiguration;import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;/** * @author Damon * @date 2020年1月13日 下午3:23:06 * */@Configuration@EnableAutoConfiguration@ComponentScan(basePackages = {\"com.damon\"})@EnableDiscoveryClient@EnableOAuth2Ssopublic class ProviderApp { public static void main(String[] args) { SpringApplication.run(ProviderApp.class, args); }} 注意:注解 @EnableDiscoveryClient、@EnableOAuth2Sso 都需要。 这时,同样需要配置 ResourceServerConfig、SecurityConfig。 如果需要数据库,可以加上: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112package com.damon.config;import java.util.Properties;import javax.sql.DataSource;import org.apache.ibatis.plugin.Interceptor;import org.apache.ibatis.session.SqlSessionFactory;import org.mybatis.spring.SqlSessionFactoryBean;import org.mybatis.spring.SqlSessionTemplate;import org.mybatis.spring.annotation.MapperScan;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.env.Environment;import org.springframework.core.io.support.PathMatchingResourcePatternResolver;import org.springframework.jdbc.datasource.DataSourceTransactionManager;import org.springframework.stereotype.Component;import org.springframework.transaction.annotation.EnableTransactionManagement;import com.alibaba.druid.pool.DruidDataSourceFactory;import com.github.pagehelper.PageHelper;/***** created by wangshoufa* 2018年5月23日 下午7:39:37**/@Component@Configuration@EnableTransactionManagement@MapperScan(\"com.damon.*.dao\")public class MybaitsConfig { @Autowired private EnvConfig envConfig; @Autowired private Environment env; @Bean(name = \"dataSource\") public DataSource getDataSource() throws Exception { Properties props = new Properties(); props.put(\"driverClassName\", envConfig.getJdbc_driverClassName()); props.put(\"url\", envConfig.getJdbc_url()); props.put(\"username\", envConfig.getJdbc_username()); props.put(\"password\", envConfig.getJdbc_password()); return DruidDataSourceFactory.createDataSource(props); } @Bean public SqlSessionFactory sqlSessionFactory(@Qualifier(\"dataSource\") DataSource dataSource) throws Exception { SqlSessionFactoryBean fb = new SqlSessionFactoryBean(); // 指定数据源(这个必须有,否则报错) fb.setDataSource(dataSource); // 下边两句仅仅用于*.xml文件,如果整个持久层操作不需要使用到xml文件的话(只用注解就可以搞定),则不加 fb.setTypeAliasesPackage(env.getProperty(\"mybatis.typeAliasesPackage\"));// 指定基包 fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(env.getProperty(\"mybatis.mapperLocations\")));// 指定xml文件位置 // 分页插件 PageHelper pageHelper = new PageHelper(); Properties props = new Properties(); // 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 //禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 props.setProperty(\"reasonable\", \"true\"); //指定数据库 props.setProperty(\"dialect\", \"mysql\"); //支持通过Mapper接口参数来传递分页参数 props.setProperty(\"supportMethodsArguments\", \"true\"); //总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page props.setProperty(\"returnPageInfo\", \"check\"); props.setProperty(\"params\", \"count=countSql\"); pageHelper.setProperties(props); // 添加插件 fb.setPlugins(new Interceptor[] { pageHelper }); try { return fb.getObject(); } catch (Exception e) { throw e; } } /** * 配置事务管理器 * @param dataSource * @return * @throws Exception */ @Bean public DataSourceTransactionManager transactionManager(DataSource dataSource) throws Exception { return new DataSourceTransactionManager(dataSource); } @Bean public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); }} 接下来新写一个 controller 类: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172package com.damon.user.controller;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.access.prepost.PreAuthorize;import org.springframework.security.core.Authentication;import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import com.damon.commons.Response;import com.damon.user.service.UserService;/** * * * @author Damon * @date 2020年1月13日 下午3:31:07 * */@RestController@RequestMapping(\"/api/user\")public class UserController { private static final Logger logger = LoggerFactory.getLogger(UserController.class); @Autowired private UserService userService; @GetMapping(\"/getCurrentUser\") @PreAuthorize(\"hasAuthority('admin')\") public Object getCurrentUser(Authentication authentication) { logger.info(\"test password mode\"); return authentication; } @PreAuthorize(\"hasAuthority('admin')\") @GetMapping(\"/auth/admin\") public Object adminAuth() { logger.info(\"test password mode\"); return \"Has admin auth!\"; } @GetMapping(value = \"/get\") @PreAuthorize(\"hasAuthority('admin')\") //@PreAuthorize(\"hasRole('admin')\")//无效 public Object get(Authentication authentication){ //Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); authentication.getCredentials(); OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails)authentication.getDetails(); String token = details.getTokenValue(); return token; } @GetMapping(\"/getUserInfo\") @PreAuthorize(\"hasAuthority('admin')\") public Response<Object> getUserInfo(Authentication authentication) { logger.info(\"test password mode\"); Object principal = authentication.getPrincipal(); if(principal instanceof String) { String username = (String) principal; return userService.getUserByUsername(username); } return null; }} 基本上一个代码就完成了。接下来测试一下: 认证: 1curl -i -X POST -d \"username=admin&password=123456&grant_type=password&client_id=provider-service&client_secret=provider-service-123\" http://localhost:5555/oauth-cas/oauth/token 拿到token后: 1curl -i -H \"Accept: application/json\" -H \"Authorization:bearer f4a42baa-a24a-4342-a00b-32cb135afce9\" -X GET http://localhost:5555/provider-service/api/user/getCurrentUser 这里用到了 5555 端口,这是一个网关服务,好吧,既然提到这个,我们接下来看网关吧,引入依赖: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\"> <modelVersion>4.0.0</modelVersion> <groupId>com.damon</groupId> <artifactId>alibaba-gateway</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>alibaba-gateway</name> <url>http://maven.apache.org</url> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.8.RELEASE</version> <relativePath/> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <swagger.version>2.6.1</swagger.version> <xstream.version>1.4.7</xstream.version> <pageHelper.version>4.1.6</pageHelper.version> <fastjson.version>1.2.51</fastjson.version> <!-- <springcloud.version>2.1.8.RELEASE</springcloud.version> --> <springcloud.version>Greenwich.SR3</springcloud.version> <springcloud.kubernetes.version>1.1.1.RELEASE</springcloud.kubernetes.version> <mysql.version>5.1.46</mysql.version> <alibaba-cloud.version>2.1.1.RELEASE</alibaba-cloud.version> <springcloud.alibaba.version>0.9.0.RELEASE</springcloud.alibaba.version> </properties> <dependencyManagement> <dependencies> <!-- <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${alibaba-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${springcloud.alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${springcloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- 不要依赖spring-boot-starter-web,会和spring-cloud-starter-gateway冲突,启动时异常 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <!--基于 reactive stream 的redis --> <!-- <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency> --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-commons</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>${fastjson.version}</version> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>19.0</version> </dependency> </dependencies> <build> <finalName>${project.artifactId}</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <jvmArguments>-Dfile.encoding=UTF-8</jvmArguments> <fork>true</fork> </configuration> </plugin> <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.7.8</version> <executions> <execution> <goals> <goal>prepare-agent</goal> <goal>report</goal> </goals> </execution> </executions> </plugin> <!-- 自动生成代码 插件 begin --> <!-- <plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.2</version> <configuration> <configurationFile>src/main/resources/generatorConfig.xml</configurationFile> <verbose>true</verbose> <overwrite>true</overwrite> </configuration> <dependencies> <dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.3.2</version> </dependency> </dependencies> </plugin> --> </plugins> </build></project> 同样利用 Nacos 来发现服务。 相关配置在 Spring Cloud Kubernetes之实战三网关Gateway 一文中有讲过,这里的注册配置改为: 123456789101112131415spring: cloud: gateway: discovery: locator: enabled: true #并且我们并没有给每一个服务单独配置路由 而是使用了服务发现自动注册路由的方式 lowerCaseServiceId: true nacos: discovery: server-addr: 127.0.0.1:8848 config: server-addr: 127.0.0.1:8848 refreshable-dataids: actuator.properties,log.properties 前面用的是 kubernetes。 好了,网关配置好后,启动在 Nacos dashboard可以看到该服务,表示注册服务成功。接下来就可以利用其来调用其他服务了。具体 curl 命令: 1curl -i -H \"Accept: application/json\" -H \"Authorization:bearer f4a42baa-a24a-4342-a00b-32cb135afce9\" -X GET http://localhost:5555/consumer-service/api/order/getUserInfo Ok,到此鉴权中心、服务提供者、服务消费者、服务的注册与发现、配置中心等功能已完成。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"Java","slug":"Java","permalink":"http://damon008.github.io/tags/Java/"},{"name":"SpringCloud Alibaba","slug":"SpringCloud-Alibaba","permalink":"http://damon008.github.io/tags/SpringCloud-Alibaba/"}]},{"title":"微服务架构设计之解耦合","date":"2020-07-23T00:47:27.000Z","path":"2020/07/23/micro-service02/","text":"背景在各个 IT 行业的公司,我们会有大大小小的业务需求。当每个产品的业务功能越来越繁重时,也许用户的需求其实很简单,就想 One Click。但是,其实这一个按钮背后可能有很多的系统交互的操作在进行,这就涉及到业务数据操作的事务,涉及到每个系统的交互逻辑、先后顺序以及数据的一致性。这些都需要在设计的时候,需要考虑到的问题。 浅谈解耦合业务系统的设计有多重要在 今天被问微服务,这几点,让面试官刮目相看 一文中,我们讲过微服务设计时的方方面面。其中核心的六个字可能就是:“高内聚,低耦合”。高内聚,我们在那篇文章中已经讲的很清楚了。那么低耦合,这就涉及到我们业务系统的设计了。所谓低耦合,就是要每个业务模块之间的关系降低,减少冗余、重复、交叉的复杂度,模块功能划分也尽可能单一。这样,才能达到低耦合的目的。 在电商行业,主要的功能就是购物,至于其他的,都是为购物作铺垫、营销手段:直播、促销、发优惠券等。从用户的角度来说,其实网上 shopping 的逻辑很简单:选中想要买的,支付 money 就 OK 了。但对于网站,或者说运营服务平台来说,其逻辑远没有那么简单。下面是一个简单的购物流程图: 在这里,我们看到,就这个简单的购物流程,对于用户来说,可能操作很简单:打开网站,登录后选择商品和选中收货地址支付,坐等收货。对于平台,其实它也不简单,包括了很多系统:用户系统、商品系统、仓库系统、订单系统、支付系统、物流系统等等。 不能仅仅因为客户的需求,只是下了一个订单,买了一件商品,那系统就设计一个就认为能解决所有事情,这种认识,可能一开始就是错的。这样的业务设计后,不但导致业务系统的逻辑很笨重,也会导致代码的 code review 非常之复杂。我曾经就亲自目睹过:好几个事情都是一个代码块来处理,甚至都写到几千行,甚至上万行。这样的思路,虽然可以实现暂时的需求。但是从长远角度,这是一个很要命的事情:这样的设计不仅仅说 code review 很吃力,兼容新功能也是很麻烦的,让后来者无法下手。而且长期下去,会导致表的死锁,甚至进入系统瘫痪状态。 如何解耦合业务的复杂性,其实根本原因是没有把其给拆解化。如果把整个的大业务拆解成若干个小的需求,那对于实现,就显得即一目了然,又能完美兼容其他任何问题。咱们还是拿购物说事,为什么每个购物 app 的系统设计都是这样的套路:选中商品后必须先加入购物车,选好地址信息,然后再统一去提交订单,最后才去支付 money 呢?难道系统直接简单点,选中后就支付不就解决了吗?那么网站何必搞得这么的麻烦,浪费时间、金钱,是为了折腾人?统统都不是。其实这也是网站开发最初想的事情,并不是说一件事情一口气能解决,就鲁莽的直接一口气解决。也许到时候,时间久了,人的精力没那么旺盛,变得虚弱的时候,那一口气就无法完成了。网站也是,一个需求也许可以简单的设计,就能完成。但是如果仅仅想着,现在简单的就完成,那是对以后的不负责任。以后可能会出现一些难以想象的事情,并且难以解决。 上面扯远了,回归到解耦合,解耦合其实有很多办法。比如 Java 中就有很多解决低耦合的方法:监听、观察模式、异步回调、定时任务、消息中间件等等。 1.1 监听在Java 里,有很多设计模式:工厂模式、单例模式、建造者模式、代理模式、解释器模式、监听模式、观察者模式等等。其中,监听模式是低耦合解决的方案之一。 所谓监听模式:事件源经过事件的封装传给监听器,当事件源触发事件后,监听器接收到事件对象可以回调事件的方法。这其中涉及到三个信息:事件源、事件、监听器。 For example : 模拟某个服务启动后,发送通知信息。 事件源: 123456789101112131415161718192021package com.damon.event;import java.util.ArrayList;import java.util.List;public class Context { private static List<Listener> list=new ArrayList<Listener>(); public static void addListener(Listener listener){ list.add(listener); } public static void removeListener(Listener listener){ list.remove(listener); } public static void sendMsg(Event event){ for(Listener listener:list){ listener.onChange(event); } }} 事件: 123456789101112131415161718192021222324package com.damon.event;public class Event { public static final int INSTALLED = 1; public static final int STARTED = 2; public static final int RESOLVED = 3; public static final int STOPPED = 4; private int type; private Object source; public Event(int type, Object source) { this.type = type; this.source = source; } public int getType() { return type; } public Object getSource() { return source; }} 监听器: 12345678910111213141516171819202122232425package com.damon.event;public class MyListener implements Listener { @Override public void onChange(Event event) { switch(event.getType()){ case Event.STARTED : System.out.println(\"started...\"); break; case Event.RESOLVED : System.out.println(\"resolved...\"); break; case Event.STOPPED : System.out.println(\"stopped...\"); break; default: throw new IllegalArgumentException(); } }} 测试: 1234567891011121314package com.damon.event;public class EventTest { public static void main(String[] args) { Listener listener = new MyListener(); //加入监听者 Context.addListener(listener); //启动完毕事件触发 Context.sendMsg(new Event(Event.STARTED, new MyBundle())); }} 在服务启动的操作中,我们不需要等待或者去处理,而是继续其他的逻辑,等到服务启动后,事件监听器监听后会进行相应的操作。这样,就不会在服务启动的过程中,需要等待其启动,因为其启动的时间是无法估量的。所以就很好的解决其耦合性的问题。避免用户在等待过程中,浪费了大量不应该由用户承担的时间成本。毕竟,对于用户来说,时间就是金钱。 1.2 观察者模式观察者模式,听着跟上面讲的监听模式有点像。但是,还是有区别的。所谓观察者模式:观察者相当于事件监听者,被观察者相当于事件源和事件,执行逻辑时通知观察者即可触发其 update,同时可传被观察者和其参数。看着是不是像简化了事件监听机制的实现。其又可以叫发布-订阅模式,只有两个角色。 For example : 微信群里发布了一条公告:下午三点开会,有些在群里的人接收到了消息去开会,但是有些人未在群里,未收到公告,被领导主动喊去开会。 观察者: 1234567891011121314public abstract class Observer { protected String name; protected Subject subject; public Observer(String name, Subject subject) { this.name = name; this.subject = subject; } public abstract void update();} 通知者: 1234567891011121314public interface Subject { //增加 public void attach(Observer observer); //删除 public void detach(Observer observer); //通知 public void notifyObservers(); //状态 public void setAction(String action); public String getAction();} 具体人:群管理员 1234567891011121314151617181920212223242526272829303132333435363738394041424344public class WechatManager implements Subject { //同事好友列表 private List<Observer> observers = new LinkedList<>(); private String action; //添加 @Override public void attach(Observer observer) { observers.add(observer); } //删除 @Override public void detach(Observer observer) { observers.remove(observer); } //通知 @Override public void notifyObservers() { for(Observer observer : observers) { observer.update(); } } //状态 @Override public String getAction() { return action; } @Override public void setAction(String action) { this.action = action; }} 具体观察者:群内人员与群外人员 1234567891011121314public class InWechatRoomObserver extends Observer { public InWechatRoomObserver(String name, Subject subject) { super(name, subject); } @Override public void update() { System.out.println(subject.getAction() + \"\\n\" + name + \"收到公告,去开会了\"); }} Test: 12345678910111213141516171819202122public class Test { public static void main(String[] args) { //群管理员为通知者 WechatManager ma = new WechatManager(); InWechatRoomObserver in = new InWechatRoomObserver(\"tom\", ma); OutWechatRoomObserver out = new OutWechatRoomObserver(\"damon\", ma); //群管理员通知 ma.attach(out); ma.attach(in); //damon没在群内,未被通知到,所以被领导发现 ma.detach(out); //老板回来了 ma.setAction(\"下午三点,大家在大会议室开会\"); //发通知 ma.notifyObservers(); }} 可以看到:当一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象有待改变的时候,可考虑使用观察者模式。 即使用观察者模式的动机在于:在保证相关业务数据的一致性,我们不希望为了维持一致性而使各个逻辑紧密耦合,这样会给维护、扩展和重用都带来不便,而观察者模式所做的工作就是在解除耦合。 1.3 异步异步,对于一个系统来说,异步操作可以很好的解耦合,因为每一步操作不需要等待结果即可继续往下进行,不论中间操作是否成功。在 Java 中,常见的异步注解:@Async,解决相应如果需要很多操作,或者操作时耗时很长,而异步进行处理来解决相关问题。有时需要注解 @EnableAsync 配合,然后弄一个异步线程池,来进行线程异步调度管理。 异步线程池初始化 bean : 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990package com.damon.task;import java.util.concurrent.Executor;import java.util.concurrent.ThreadPoolExecutor;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.scheduling.annotation.EnableAsync;import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;/** * 异步任务执行bean * * @author Damon * */@EnableAsync@Configurationpublic class TaskPoolConfig { @Bean(\"taskExecutor\") public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(20); executor.setQueueCapacity(200); executor.setKeepAliveSeconds(60); executor.setThreadNamePrefix(\"taskExecutor-\"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); return executor; }}异步调度方法类:package com.damon.task;import org.apache.commons.lang.StringUtils;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.http.*;import org.springframework.scheduling.annotation.Async;import org.springframework.stereotype.Component;import java.math.BigDecimal;import java.text.DecimalFormat;import java.util.ArrayList;import java.util.List;import java.util.Random;/** * * 远程业务调用封装类 * * @author Damon * @date 2019年3月19日 下午3:29:45 * */@Componentpublic class TaskService { private Logger logger = LoggerFactory.getLogger(getClass()); public static Random random = new Random(); /** * @description 异步任务计算耗时 * @param start 开始时间 * @param userId 用户id * @throws Exception */ @Async(\"taskExecutor\") public void doTaskOne(long start, String userId) throws Exception { logger.info(\" 开始做任务一 to {}\", start); Thread.sleep(random.nextInt(10000)); long end = System.currentTimeMillis(); logger.info(\"完成任务一,耗时:\" + (end - start) + \"毫秒\"); }} 异步可以常见于很多业务,比如异步发送短讯告诉用户,支付成功,异步发送日志到 ELK 系统等。 1.4 定时任务对于定时任务,就是指制定系统的某个时刻或每隔一段时间去触发一些逻辑执行,这样来保证业务数据的一致性,消息的一致性,或者数据的实时性。 我们常在 Java 里用 @EnableScheduling 来引入定时器,然后定义一个异步定时调度 bean: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859package com.damon.task;import java.util.concurrent.Executor;import java.util.concurrent.ThreadPoolExecutor;import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.scheduling.annotation.AsyncConfigurer;import org.springframework.scheduling.annotation.EnableAsync;import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;/** * * 异步任务执行bean * @author Damon * @date 2019年7月17日 上午10:35:56 * */@EnableAsync@Configurationpublic class TaskPoolConfig implements AsyncConfigurer { @Bean(\"asyncTask\") public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); //线程池维护线程的最少数量 executor.setCorePoolSize(10); //线程池维护线程的最大数量 executor.setMaxPoolSize(20); // 缓存队列 executor.setQueueCapacity(200); //允许的空闲时间 executor.setKeepAliveSeconds(60); executor.setThreadNamePrefix(\"asyncTask-\"); //对拒绝task的处理策略 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); return executor; } @Override public Executor getAsyncExecutor() { return null; } @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return null; }} 同时,定义一个执行类: 12345678910111213141516171819202122232425262728293031/** * * 执行调度 * @author Damon * @date 2019年3月19日 下午3:29:45 * */@Componentpublic class TaskSchedule { private Logger logger = LoggerFactory.getLogger(TaskSchedule.class); @Autowired private RestTemplate restTemplate; @Autowired private Environment env; //@Scheduled(cron = \"0 0/1 * * * ?\")//每分钟 @Scheduled(cron = \"0 * * * * ?\")//每分钟 public void dynamicResourceListener() { logger.info(\"resourceLimitHandle timer start\"); String namespace = env.getProperty(\"INFERENCE_JOB_NAMESPACE\"); resourceListenerCallBack(namespace); } private void resourceListenerCallBack(String namespace) { }} 其中,cron 从左到右(用空格隔开): 1秒 分 小时 月份中的日期 月份 星期中的日期 年份 上面的逻辑是每分钟去执行某个逻辑,这样的业务我们也可能存在,For example:股票系统中,建模等数据一般都是用 Oracle 来存储的,有时候业务可能是用 Mysql,这时,需要一个定时任务来跑数据,常见的叫 ETL,所以 ETL 的由来,就是这样来的。这样的操作肯定不能在发生业务操作时来进行,否则会因为业务数据的海量读取,导致 IO 的性能,甚至内存、CPU 都会飙升。再如统计某个业务场景的数据,都可以通过这种解耦合的方式来处理。 1.5 消息中间件消息中间件的话,这个也是很多的,比如:redis、rocketmq、rabbitmq、zk等等。这些中间件技术都可以再一个复杂的业务流程起到至关重要得作用。 当我们需要做一个秒杀的功能时,可以用 redis 来作分布式锁,这个能起到缓冲系统压力的作用,同时可以做到秒杀锁。 当我们需要在处理一些业务逻辑时,需要告知其他方,这时候可以用 MQ 来作消息处理,防止处理流程的断续。 当我们需要发送一些消息给外部时,但又不希望耽误当前的业务处理,这时候,可以用 MQ 或 redis 来处理消息。 当我们。。。任何时候,都可以用消息中间件来作降低系统间的耦合。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"后端","slug":"后端","permalink":"http://damon008.github.io/tags/%E5%90%8E%E7%AB%AF/"}]},{"title":"浅谈 Java 集合 | 底层源码解析","date":"2020-07-23T00:41:01.000Z","path":"2020/07/23/core-java02/","text":"在 Java 中,我们经常会使用到一些处理缓存数据的集合类,这些集合类都有自己的特点,今天主要分享下 Java 集合中几种经常用的 Map、List、Set。 目录 1、Map 一、背景 二、Map家族 三、HashMap、Hashtable等 四、HashMap 底层数据结构 2、List 一、List 包括的子类 二、ArrayList 三、ArrayList 源码分析 四、LinkedList 五、LinkedList 源码分析 3、Set 一、Set的实质 二、HashSet 三、TreeSet 集合 1:Map 背景 如果一个海量的数据中,需要查询某个指定的信息,这时候,可能会犹如大海捞针,这时候,可以使用 Map 来进行一个获取。因为 Map 是键值对集合。Map这种键值(key-value)映射表的数据结构,作用就是通过key能够高效、快速查找value。 举一个例子: import java.util.HashMap;import java.util.Map;import java.lang.Object; public class Test { public static void main(String[] args) { Object o = new Object(); Map<String, Object> map = new HashMap<>(); map.put(“aaa”, o); //将”aaa”和 Object实例映射并关联 Student target = map.get(“aaa”); //通过key查找并返回映射的Obj实例 System.out.println(target == o); //true,同一个实例 Student another = map.get(“bbb”); //通过另一个key查找 System.out.println(another); //未找到则返回null }}Map<K, V>是一种键-值映射表,当我们调用put(K key, V value)方法时,就把key和value做了映射并放入Map。当我们调用V get(K key)时,就可以通过key获取到对应的value。如果key不存在,则返回null。和List类似,Map也是一个接口,最常用的实现类是HashMap。 在 Map<K, V> 中,如果遍历的时候,其 key 是无序的,如何理解: import java.util.HashMap;import java.util.Map;public class Test { public static void main(String[] args) { Map<String, String> map = new HashMap<>(); map.put(“dog”, “a”); map.put(“pig”, “b”); map.put(“cat”, “c”); for (Map.Entry<String, Integer> entry : map.entrySet()) { String key = entry.getKey(); Integer value = entry.getValue(); System.out.println(key + “ = “ + value); } }} //printcat = cdog = apig = b从上面的打印结果来看,其是无序的,有序的答案可以在下面找到。接下来我们分析下 Map ,首先我们先看看 Map 家族: 它的子孙下面有我们常用的 HashMap、LinkedHashMap,也有 TreeMap,另外还有继承 Dictionary、实现 Map 接口的 Hashtable。 下面针对各个实现类的特点来说明: (1)HashMap:它根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有高效的访问速度,但遍历顺序却是不确定的。HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap 非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections 的静态方法 synchronizedMap 方法使 HashMap 具有线程安全的能力,或者使用 ConcurrentHashMap(分段加锁)。 (2)LinkedHashMap:LinkedHashMap 是 HashMap 的一个子类,替 HashMap 完成了输入顺序的记录功能,所以要想实现像输出同输入顺序一致,应该使用 LinkedHashMap。 (3)TreeMap:TreeMap 实现 SortedMap 接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用 Iterator 遍历 TreeMap 时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用 TreeMap 时,key 必须实现Comparable 接口或者在构造 TreeMap 传入自定义的 Comparator,否则会在运行时抛出 ClassCastException 类型的异常。 (4)Hashtable:Hashtable继承 Dictionary 类,实现 Map 接口,很多映射的常用功能与 HashMap 类似,Hashtable 采用”拉链法”实现哈希表,不同的是它来自 Dictionary 类,并且是线程安全的,任一时间只有一个线程能写 Hashtable,但并发性不如 ConcurrentHashMap,因为ConcurrentHashMap 引入了分段锁。Hashtable 使用 synchronized 来保证线程安全,在线程竞争激烈的情况下 HashTable 的效率非常低下。不建议在新代码中使用,不需要线程安全的场合可以用 HashMap 替换,需要线程安全的场合可以用 ConcurrentHashMap 替换。Hashtable 并不是像 ConcurrentHashMap 对数组的每个位置加锁,而是对操作加锁,性能较差。 上面讲到了 HashMap、Hashtable、ConcurrentHashMap,接下来先看看 HashMap 的源码实现: public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { private static final long serialVersionUID = 362498820763181265L; /** * 默认大小 16 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; /** * 最大容量是必须是2的幂30 */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * 负载因子默认为0.75,hashmap每次扩容为原hashmap的2倍 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 链表的最大长度为8,当超过8时会将链表装换为红黑树进行存储 */ static final int TREEIFY_THRESHOLD = 8; /** * The table, initialized on first use, and resized as * necessary. When allocated, length is always a power of two. * (We also tolerate length zero in some operations to allow * bootstrapping mechanics that are currently not needed.) */ transient Node<K,V>[] table; static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + \"=\" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } } 从上面看到,HashMap 主要是数组 + 链表结构组成。HashMap 扩容是成倍的扩容。为什么是成倍,而不是1.5或其他的倍数呢?既然 HashMap 在进行 put 的时候针对 key 做了一些列的 hash 以及与运算就是为了减少碰撞的一个概率,如果扩容后的大小不是2的n次幂的话,之前做的不是白费了吗? else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1;扩容后会重新把原来的所有的数据 key 的 hash 重新计算放入扩容后的数组里面去。为什么要这样做?因为不同的数组大小通过 key 的 hash 出来的下标是不一样的。还有,数组长度保持2的次幂,length-1的低位都为1,会使得获得的数组索引 index 更加均匀。 为何说 Hashmap 是非线程安全的呢?原因:当多线程并发时,检测到总数量超过门限值的时候就会同时调用 resize 操作,各自生成新的数组并rehash 后赋给底层数组,结果最终只有最后一个线程生成的新数组被赋给table 变量,其他线程均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的 table 作为原始数组,这样也是有问题滴。 疑问: HashMap 中某个 entry 链过长,查询时间达到最大限度,如何处理呢?这个在 Jdk1.8,当链表过长,把链表转成红黑树(TreeNode)实现了更高的时间复杂度的查找。 HashMap中哈希算法实现?我们使用put(key,value)方法往HashMap中添加元素时,先计算得到key的 Hash 值,然后通过Key高16位与低16位相异或(高16位不变),然后与数组大小-1相与,得到了该元素在数组中的位置,流程: 延伸:如果一个对象中,重写了equals()而不重写hashcode()会发生什么样的问题?尽管我们在进行 get 和 put 操作的时候,使用的key从逻辑上讲是等值的(通过equals比较是相等的),但由于没有重写hashCode(),所以put操作时,key(hashcode1)–>hash–>indexFor–>index,而通过key取出value的时候 key(hashcode2)–>hash–>indexFor–>index,由于hashcode1不等于hashcode2,导致没有定位到一个数组位置而返回逻辑上错误的值null。所以,在重写equals()的时候,必须注意重写hashCode(),同时还要保证通过equals()判断相等的两个对象,调用hashCode方法要返回同样的整数值。而如果equals判断不相等的两个对象,其hashCode也可以相同的(只不过会发生哈希冲突,应尽量避免)。(1. hash相同,但key不一定相同:key1、key2产生的hash很有可能是相同的,如果key真的相同,就不会存在散列链表了,散列链表是很多不同的键算出的hash值和index相同的 2. key相同,经过两次hash,其hash值一定相同) ConcurrentHashMap 采用了分段锁技术来将数据分成一段段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。 02 集合 2:List 集合 List 是接口 Collection 的子接口,也是大家经常用到的数据缓存。List 进行了元素排序,且允许存放相同的元素,即有序,可重复。我们先看看有哪些子类: 可以看到,其中包括比较多的子类,我们常用的是 ArrayList、LinkedList: ArrayList: 优点:操作读取操作效率高,基于数组实现的,可以为null值,可以允许重复元素,有序,异步。 缺点:由于它是由动态数组实现的,不适合频繁的对元素的插入和删除操作,因为每次插入和删除都需要移动数组中的元素。 LinkedList: 优点:LinkedList由双链表实现,增删由于不需要移动底层数组数据,其底层是链表实现的,只需要修改链表节点指针,对元素的插入和删除效率较高。 缺点:遍历效率较低。HashMap和双链表也有关系。 ArrayList 底层是一个变长的数组,基本上等同于Vector,但是ArrayList对writeObjec() 和 readObject()方法实现了同步。 transient Object[] elementData; /** Constructs an empty list with an initial capacity of ten. */public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}If multiple threads access an ArrayList instance concurrently, and at least one of the threads modifies the list structurally, it must be synchronized externally.(A structural modification is any operation that adds or deletes one or more elements, or explicitly resizes the backing array; merely setting the value of an element is not a structural modification.)This is typically accomplished by synchronizing on some object that naturally encapsulates the list.从注释,我们知道 ArrayList 是线程不安全的,多线程环境下要通过外部的同步策略后使用,比如List list = Collections.synchronizedList(new ArrayList(…))。 源码实现: private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ // Write out element count, and any hidden stuff int expectedModCount = modCount; s.defaultWriteObject(); // Write out size as capacity for behavioural compatibility with clone() s.writeInt(size); // Write out all elements in the proper order. for (int i=0; i<size; i++) { s.writeObject(elementData[i]); } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); }} /** Reconstitute the ArrayList instance from a stream (that is, deserialize it). */private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { elementData = EMPTY_ELEMENTDATA; // Read in size, and any hidden stuff s.defaultReadObject(); // Read in capacity s.readInt(); // ignored if (size > 0) { // be like clone(), allocate array based upon size not capacity int capacity = calculateCapacity(elementData, size); SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity); ensureCapacityInternal(size); Object[] a = elementData; // Read in all elements in the proper order. for (int i=0; i<size; i++) { a[i] = s.readObject(); } }}当调用add函数时,会调用ensureCapacityInternal函数进行扩容,每次扩容为原来大小的1.5倍,但是当第一次添加元素或者列表中元素个数小于10的话,列表容量默认为10。 /** Default initial capacity. */private static final int DEFAULT_CAPACITY = 10; /** Shared empty array instance used for empty instances. */private static final Object[] EMPTY_ELEMENTDATA = {}; /** Shared empty array instance used for default sized empty instances. */private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; /** The array buffer into which the elements of the ArrayList are stored. */transient Object[] elementData; // non-private to simplify nested class access /** The size of the ArrayList (the number of elements it contains). */private int size;扩容原理:根据当前数组的大小,判断是否小于默认值10,如果大于,则需要扩容至当前数组大小的1.5倍,重新将新扩容的数组数据copy只当前elementData,最后将传入的元素赋值给size++位置。 private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));} private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity);} /** The maximum size of array to allocate. Some VMs reserve some header words in an array. Attempts to allocate larger arrays may result in OutOfMemoryError: Requested array size exceeds VM limit */private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; /** Increases the capacity to ensure that it can hold at least the number of elements specified by the minimum capacity argument. @param minCapacity the desired minimum capacity */private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity);} private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;}接下来我们分析为什么 ArrayList 增删很慢,查询很快呢? public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true;}根据源码可知,当调用add函数时,首先要调用ensureCapacityInternal(size + 1),该函数是进行自动扩容的,效率低的原因也就是在这个扩容上了,每次新增都要对现有的数组进行一次1.5倍的扩大,数组间值的copy等,最后等扩容完毕,有空间位置了,将数组size+1的位置放入元素e,实现新增。 删除时源码: /** Removes the element at the specified position in this list. Shifts any subsequent elements to the left (subtracts one from their indices). @param index the index of the element to be removed @return the element that was removed from the list @throws IndexOutOfBoundsException {@inheritDoc} */public E remove(int index) { rangeCheck(index); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[–size] = null; return oldValue;}在删除index位置的元素时,要先调用 rangeCheck(index) 进行 index 的check,index 要超过当前个数,则判定越界,抛出异常,throw new IndexOutOfBoundsException(outOfBoundsMsg(index)),其他函数也有用到如:get(int index),set(int index, E element) 等后面删除重点在于计算删除的index是末尾还是中间位置,末尾直接–,然后置空完事,如果是中间位置,那就要进行一个数组间的copy,重新组合数组数据了,这一就比较耗性能了。 而查询: /** Returns the element at the specified position in this list. @param index index of the element to return @return the element at the specified position in this list @throws IndexOutOfBoundsException {@inheritDoc} */public E get(int index) { rangeCheck(index); return elementData(index);}获取指定index的元素,首先调用rangeCheck(index)进行index的check,通过后直接获取数组的下标index获取数据,没有任何多余操作,高效。 LinkedList 继承AbstractSequentialList和实现List接口,新增接口如下: addFirst(E e):将指定元素添加到刘表开头 addLast(E e):将指定元素添加到列表末尾 descendingIterator():以逆向顺序返回列表的迭代器 element():获取但不移除列表的第一个元素 getFirst():返回列表的第一个元素 getLast():返回列表的最后一个元素 offerFirst(E e):在列表开头插入指定元素 offerLast(E e):在列表尾部插入指定元素 peekFirst():获取但不移除列表的第一个元素 peekLast():获取但不移除列表的最后一个元素 pollFirst():获取并移除列表的最后一个元素 pollLast():获取并移除列表的最后一个元素 pop():从列表所表示的堆栈弹出一个元素 push(E e);将元素推入列表表示的堆栈 removeFirst():移除并返回列表的第一个元素 removeLast():移除并返回列表的最后一个元素 removeFirstOccurrence(E e):从列表中移除第一次出现的指定元素 removeLastOccurrence(E e):从列表中移除最后一次出现的指定元素LinkedList 的实现原理:LinkedList 的实现是一个双向链表。在 Jdk 1.6中是一个带空头的循环双向链表,而在 Jdk1.7+ 中则变为不带空头的双向链表,这从源码中可以看出: //jdk 1.6private transient Entry header = new Entry(null, null, null);private transient int size = 0; //jdk 1.7transient int size = 0; transient Node first; transient Node last;从源码注释看,LinkedList不是线程安全的,多线程环境下要通过外部的同步策略后使用,比如List list = Collections.synchronizedList(new LinkedList(…)): If multiple threads access a linked list concurrently, and at least one of the threads modifies the list structurally, it must be synchronized externally. (A structural modification is any operation that adds or deletes one or more elements; merely setting the value of an element is not a structural modification.) This is typically accomplished by synchronizing on some object that naturally encapsulates the list.为什么说 LinkedList 增删很快呢? /** Appends the specified element to the end of this list. This method is equivalent to {@link #addLast}. @param e element to be appended to this list @return {@code true} (as specified by {@link Collection#add}) /public boolean add(E e) { linkLast(e); return true;}/* Links e as last element. */void linkLast(E e) { final Node l = last; final Node newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++;}从注释看,add函数实则是将元素append至list的末尾,具体过程是:新建一个Node节点,其中将后面的那个节点last作为新节点的前置节点,后节点为null;将这个新Node节点作为整个list的后节点,如果之前的后节点l为null,将新建的Node作为list的前节点,否则,list的后节点指针指向新建Node,最后size+1,当前llist操作数modCount+1。 在add一个新元素时,LinkedList 所关心的重要数据,一共两个变量,一个first,一个last,这大大提升了插入时的效率,且默认是追加至末尾,保证了顺序。 再看删除一个元素: /** Removes the element at the specified position in this list. Shifts any subsequent elements to the left (subtracts one from their indices). Returns the element that was removed from the list. @param index the index of the element to be removed @return the element previously at the specified position @throws IndexOutOfBoundsException {@inheritDoc} */public E remove(int index) { checkElementIndex(index); return unlink(node(index));} /** Unlinks non-null node x. */E unlink(Node x) { // assert x != null; final E element = x.item; final Node next = x.next; final Node prev = x.prev; if (prev == null) { first = next; } else { prev.next = next; x.prev = null; } if (next == null) { last = prev; } else { next.prev = prev; x.next = null; } x.item = null; size–; modCount++; return element;}删除指定index的元素,删除之前要调用checkElementIndex(index)去check一下index是否存在元素,如果不存在抛出throw new IndexOutOfBoundsException(outOfBoundsMsg(index));越界错误,同样这个check方法也是很多方法用到的,如:get(int index),set(int index, E element)等。 注释讲,删除的是非空的节点,这里的node节点也是通过node(index)获取的,分别根据当前Node得到链表上的关节要素:element、next、prev,分别对 prev 和 next 进行判断,以便对当前 list 的前后节点进行重新赋值,frist和last,最后将节点的element置为null,个数-1,操作数+1。根据以上分析,remove节点关键的变量,是Node实例本身的局部变量 next、prev、item 重新构建内部变量指针指向,以及list的局部变量first和last保证节点相连。这些变量的操作使得其删除动作也很高效。 而对于查询: /** Returns the element at the specified position in this list. @param index index of the element to return @return the element at the specified position in this list @throws IndexOutOfBoundsException {@inheritDoc} */public E get(int index) { checkElementIndex(index); return node(index).item;}获取指定index位置的node,获取之前还是调用checkElementIndex(index)进行检查元素,之后通过node(index)获取元素,上文有提到,node的获取是遍历得到的元素,所以相对性能效率会低一些。 03 集合 3:Set Set 集合在我们日常中,用到的也比较多。用于存储不重复的元素集合,它主要提供下面几种方法: 将元素添加进Set:add(E e) 将元素从Set删除:remove(Object e) 判断是否包含元素:contains(Object e) 这几种方法返回结果都是 boolean值,即返回是否正确或成功。Set 相当于只存储key、不存储value的Map。我们经常用 Set 用于去除重复元素,因为 重复add同一个 key 时,会返回 false。 public HashSet() { map = new HashMap<>();} public TreeSet() { this(new TreeMap<E,Object>());}Set 子孙中主要有:HashSet、SortedSet。HashSet是无序的,因为它实现了Set接口,并没有实现SortedSet接口,而TreeSet 实现了SortedSet接口,从而保证元素是有序的。 HashSet 添加后输出也是无序的: public class Test { public static void main(String[] args) { Set set = new HashSet<>(); set.add(“2”); set.add(“6”); set.add(“44”); set.add(“5”); for (String s : set) { System.out.println(s); } }} //print44256看到输出的顺序既不是添加的顺序,也不是String排序的顺序,在不同版本的JDK中,这个顺序也可能是不同的。 换成TreeSet: public static void main(String[] args) { Set set = new TreeSet<>(); set.add(“2”); set.add(“6”); set.add(“44”); set.add(“5”); for (String s : set) { System.out.println(s); } }//print24456 在遍历TreeSet时,输出就是有序的,不是添加时的顺序,而是元素的排序顺序。 注意:添加的元素必须实现Comparable接口,如果没有实现Comparable接口,那么创建TreeSet时必须传入一个Comparator对象。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"Java","slug":"Java","permalink":"http://damon008.github.io/tags/Java/"}]},{"title":"大佬整理的mysql规范,分享给大家","date":"2020-07-23T00:21:08.000Z","path":"2020/07/23/mysql-norm/","text":"最近涉及数据库相关操作较多,公司现有规范也不是太全面,就根据网上各路大神的相关规范,整理了一些自用的规范用法,万望指正。数据库环境dev: 开发环境开发可读写,可修改表结构。开发人员可以修改表结构,可以随意修改其中的数据但是需要保证不影响其他开发同事。 test: 测试环境开发可读写,开发人员可以通过工具修改表结构。 online: 线上环境开发人员不允许直接在线上环境进行数据库操作,如果需要操作必须找DBA进行操作并进行相应记录,禁止进行压力测试。重点的问题,各个环境的mysql服务器对应的用户权限,一定要做到权限划分明确,有辨识度,能具体区分业务场景等。 命名规范基本命名规则使用有意义的英文词汇,词汇中间以下划线分隔。(不要用拼音)只能使用英文字母,数字,下划线,并以英文字母开头。库、表、字段全部采用小写,不要使用驼峰式命名。避免用ORACLE、MySQL的保留字,如desc,关键字如index。命名禁止超过32个字符,须见名之意,建议使用名词不是动词数据库,数据表一律使用前缀临时库、表名必须以tmp为前缀,并以日期为后缀备份库、表必须以bak为前缀,并以日期为后缀为什么库、表、字段全部采用小写?在 MySQL 中,数据库和表对就于那些目录下的目录和文件。因而,操作系统的敏感性决定数据库和表命名的大小写敏感。Windows下是不区分大小写的。Linux下大小写规则数据库名与表名是严格区分大小写的;表的别名是严格区分大小写的;列名与列的别名在所有的情况下均是忽略大小写的;变量名也是严格区分大小写的;如果已经设置了驼峰式的命名如何解决?需要在MySQL的配置文件my.ini中增加 lower_case_table_names = 1即可。 表命名同一个模块的表尽可能使用相同的前缀,表名称尽可能表达含义。所有日志表均以 log_ 开头 字段命名表达其实际含义的英文单词或简写。布尔意义的字段以is_作为前缀,后接动词过去分词。各表之间相同意义的字段应同名。各表之间相同意义的字段,以去掉模块前缀的表名_字段名命名。外键字段用表名_字段名表示其关联关系。表的主键一般都约定成为id,自增类型,是别的表的外键均使用xxx_id的方式来表明。 索引命名非唯一索引必须按照“idx_字段名称_字段名称[_字段名]”进行命名唯一索引必须按照“uniq_字段名称_字段名称[_字段名]”进行命名 约束命名主键约束:pk_表名称。唯一约束:uk_表名称_字段名。(应用中需要同时有唯一性检查逻辑。) 表设计规范表引擎取决于实际应用场景;日志及报表类表建议用myisam,与交易,审核,金额相关的表建议用innodb引擎。如无说明,建表时一律采用innodb引擎默认使用utf8mb4字符集,数据库排序规则使用utf8mb4_general_ci,(由于数据库定义使用了默认,数据表可以不再定义,但为保险起见,建议都写上为什么字符集不选择utf8,排序规则不使用utf8_general_ci采用utf8编码的MySQL无法保存占位是4个字节的Emoji表情。为了使后端的项目,全面支持客户端输入的Emoji表情,升级编码为utf8mb4是最佳解决方案。对于JDBC连接串设置了characterEncoding为utf8或者做了上述配置仍旧无法正常插入emoji数据的情况,需要在代码中指定连接的字符集为utf8mb4。所有表、字段均应用 comment 列属性来描述此表、字段所代表的真正含义,如枚举值则建议将该字段中使用的内容都定义出来。如无说明,表中的第一个id字段一定是主键且为自动增长,禁止在非事务内作为上下文作为条件进行数据传递。禁止使用varchar类型作为主键语句设计。如无说明,表必须包含create_time和modify_time字段,即表必须包含记录创建时间和修改时间的字段如无说明,表必须包含is_del,用来标示数据是否被删除,原则上数据库数据不允许物理删除。用尽量少的存储空间来存数一个字段的数据能用int的就不用char或者varchar能用tinyint的就不用int使用UNSIGNED存储非负数值。不建议使用ENUM、SET类型,使用TINYINT来代替使用短数据类型,比如取值范围为0-80时,使用TINYINT UNSIGNED存储精确浮点数必须使用DECIMAL替代FLOAT和DOUBLE时间字段,除特殊情况一律采用int来记录unix_timestamp存储年使用YEAR类型。存储日期使用DATE类型。存储时间(精确到秒)建议使用TIMESTAMP类型,因为TIMESTAMP使用4字节,DATETIME使用8个字节。建议使用INT UNSIGNED存储IPV4。尽可能不使用TEXT、BLOB类型禁止在数据库中使用VARBINARY、BLOB存储图片、文件等。建议使用其他方式存储(TFS/SFS),MySQL只保存指针信息。单条记录大小禁止超过8k(列长度(中文)_3(UTF8)+列长度(英文)_1) datetime与timestamp有什么不同?相同点:TIMESTAMP列的显示格式与DATETIME列相同。显示宽度固定在19字符,并且格式为YYYY-MM-DD HH:MM:SS。 不同点:TIMESTAMP4个字节储存,时间范围:1970-01-01 08:00:01 ~ 2038-01-19 11:14:07 值以UTC格式保存,涉及时区转化 ,存储时对当前的时区进行转换,检索时再转换回当前的时区。datetime 8个字节储存,时间范围:1000-01-01 00:00:00 ~ 9999-12-31 23:59:59实际格式储存,与时区无关 如何使用TIMESTAMP的自动赋值属性?将当前时间作为ts的默认值:ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP。当行更新时,更新ts的值:ts TIMESTAMP DEFAULT 0 ON UPDATE CURRENT_TIMESTAMP。可以将1和2结合起来:ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP。如何使用INT UNSIGNED存储ip?使用INT UNSIGNED而不是char(15)来存储ipv4地址,通过MySQL函数inet_ntoa和inet_aton来进行转化。Ipv6地址目前没有转化函数,需要使用DECIMAL或者两个bigINT来存储。如无备注,所有字段都设置NOT NULL,并设置默认值;禁止在数据库中存储明文密码如无备注,所有的布尔值字段,如is_hot、is_deleted,都必须设置一个默认值,并设为0;如无备注,排序字段order_id在程序中默认使用降序排列;整形定义中不添加长度,比如使用INT,而不是INT[4]INT[M],M值代表什么含义?注意数值类型括号后面的数字只是表示宽度而跟存储范围没有关系。很多人他们认为INT(4)和INT(10)其取值范围分别是 (-9999到9999)和(-9999999999到9999999999),这种理解是错误的。其实对整型中的 M值与 ZEROFILL 属性结合使用时可以实现列值等宽。不管INT[M]中M值是多少,其取值范围还是 (-2147483648到2147483647 有符号时),(0到4294967295无符号时)。显示宽度并不限制可以在列内保存的值的范围,也不限制超过列的指定宽度的值的显示。当结合可选扩展属性ZEROFILL使用时默认补充的空格用零代替。例如:对于声明为INT(5) ZEROFILL的列,值4检索为00004。请注意如果在整数列保存超过显示宽度的一个值,当MySQL为复杂联接生成临时表时会遇到问题,因为在这些情况下MySQL相信数据适合原列宽度,如果为一个数值列指定ZEROFILL, MySQL自动为该列添加UNSIGNED属性。使用VARBINARY存储大小写敏感的变长字符串什么时候用CHAR,什么时候用VARCHAR?CHAR和VARCHAR类型类似,但它们保存和检索的方式不同。它们的最大长度和是否尾部空格被保留等方面也不同。CHAR和VARCHAR类型声明的长度表示你想要保存的最大字符数。例如,CHAR(30)可以占用30个字符。CHAR列的长度固定为创建表时声明的长度。长度可以为从0到255的任何值。当保存CHAR值时,在它们的右边填充空格以达到指定的长度。当检索到CHAR值时,尾部的空格被删除掉。在存储或检索过程中不进行大小写转换。VARCHAR列中的值为可变长字符串。长度可以指定为0到65,535之间的值。(VARCHAR的最大有效长度由最大行大小和使用的字符集确定。整体最大长度是65,532字节)。同CHAR对比,VARCHAR值保存时只保存需要的字符数,另加一个字节来记录长度(如果列声明的长度超过255,则使用两个字节)。VARCHAR值保存时不进行填充。当值保存和检索时尾部的空格仍保留,符合标准SQL。char适合存储用户密码的MD5哈希值,它的长度总是一样的。对于经常改变的值,char也好于varchar,因为固定长度的行不容易产生碎片,对于很短的列,char的效率也高于varchar。char(1)字符串对于单字节字符集只会占用一个字节,但是varchar(1)则会占用2个字节,因为1个字节用来存储长度信息。 索引设计规范MySQL的查询速度依赖良好的索引设计,因此索引对于高性能至关重要。合理的索引会加快查询速度(包括UPDATE和DELETE的速度,MySQL会将包含该行的page加载到内存中,然后进行UPDATE或者DELETE操作),不合理的索引会降低速度。MySQL索引查找类似于新华字典的拼音和部首查找,当拼音和部首索引不存在时,只能通过一页一页的翻页来查找。当MySQL查询不能使用索引时,MySQL会进行全表扫描,会消耗大量的IO。索引的用途:去重、加速定位、避免排序、覆盖索引。 什么是覆盖索引InnoDB存储引擎中,secondary index(非主键索引)中没有直接存储行地址,存储主键值。如果用户需要查询secondary index中所不包含的数据列时,需要先通过secondary index查找到主键值,然后再通过主键查询到其他数据列,因此需要查询两次。覆盖索引的概念就是查询可以通过在一个索引中完成,覆盖索引效率会比较高,主键查询是天然的覆盖索引。合理的创建索引以及合理的使用查询语句,当使用到覆盖索引时可以获得性能提升。比如SELECT email,uid FROM user_email WHERE uid=xx,如果uid不是主键,适当时候可以将索引添加为index(uid,email),以获得性能提升。 索引的基本规范索引数量控制,单张表中索引数量不超过5个,单个索引中的字段数不超过5个。综合评估数据密度和分布 考虑查询和更新比例为什么一张表中不能存在过多的索引?InnoDB的secondary index使用b+tree来存储,因此在UPDATE、DELETE、INSERT的时候需要对b+tree进行调整,过多的索引会减慢更新的速度。对字符串使用前缀索引,前缀索引长度不超过8个字符,建议优先考虑前缀索引,必要时可添加伪列并建立索引。不要索引blob/text等字段,不要索引大型字段,这样做会让索引占用太多的存储空间 什么是前缀索引?前缀索引说白了就是对文本的前几个字符(具体是几个字符在建立索引时指定)建立索引,这样建立起来的索引更小,所以查询更快。前缀索引能有效减小索引文件的大小,提高索引的速度。但是前缀索引也有它的坏处:MySQL 不能在 ORDER BY 或 GROUP BY 中使用前缀索引,也不能把它们用作覆盖索引(Covering Index)。建立前缀索引的语法: 1ALTER TABLE table_name ADD KEY(column_name(prefix_length)); 主键准则表必须有主键不使用更新频繁的列尽量不选择字符串列不使用UUID MD5 HASH默认使用非空的唯一键建议选择自增或发号器重要的SQL必须被索引,核心SQL优先考虑覆盖索索引UPDATE、DELETE语句的WHERE条件列ORDER BY、GROUP BY、DISTINCT的字段多表JOIN的字段区分度最大的字段放在前面选择筛选性更优的字段放在最前面,比如单号、userid等,type,status等筛选性一般不建议放在最前面索引根据左前缀原则,当建立一个联合索引(a,b,c),则查询条件里面只有包含(a)或(a,b)或(a,b,c)的时候才能走索引,(a,c)作为条件的时候只能使用到a列索引,所以这个时候要确定a的返回列一定不能太多,不然语句设计就不合理,(b,c)则不能走索引合理创建联合索引(避免冗余),(a,b,c) 相当于 (a) 、(a,b) 、(a,b,c) 索引禁忌不在低基数列上建立索引,例如“性别”不在索引列进行数学运算和函数运算不要索引常用的小型表尽量不使用外键外键用来保护参照完整性,可在业务端实现对父表和子表的操作会相互影响,降低可用性INNODB本身对online DDL的限制MYSQL 中索引的限制MYISAM 存储引擎索引长度的总和不能超过 1000 字节BLOB 和 TEXT 类型的列只能创建前缀索引MYSQL 目前不支持函数索引使用不等于 (!= 或者 <>) 的时候, MYSQL 无法使用索引。过滤字段使用函数运算 (如 abs (column)) 后, MYSQL无法使用索引。join语句中join条件字段类型不一致的时候MYSQL无法使用索引使用 LIKE 操作的时候如果条件以通配符开始 (如 ‘%abc…’)时, MYSQL无法使用索引。使用非等值查询的时候, MYSQL 无法使用 Hash 索引。 语句设计规范使用预编译语句只传参数,比传递SQL语句更高效一次解析,多次使用降低SQL注入概率避免隐式转换会导致索引失效充分利用前缀索引必须是最左前缀不可能同时用到两个范围条件不使用%前导的查询,如like “%ab”不使用负向查询,如not in/like无法使用索引,导致全表扫描全表扫描导致buffer pool利用率降低避免使用存储过程、触发器、UDF、events等让数据库做最擅长的事降低业务耦合度,为sacle out、sharding留有余地避开BUG避免使用大表的JOINMySQL最擅长的是单表的主键/二级索引查询JOIN消耗较多内存,产生临时表避免在数据库中进行数学运算MySQL不擅长数学运算和逻辑判断无法使用索引减少与数据库的交互次数 123INSERT … ON DUPLICATE KEY UPDATEREPLACE INTO、INSERT IGNORE 、INSERT INTO VALUES(),(),()UPDATE … WHERE ID IN(10,20,50,…) 合理的使用分页限制分页展示的页数 只能点击上一页、下一页 采用延迟关联 如何正确的使用分页?假如有类似下面分页语句:SELECT * FROM table ORDER BY id LIMIT 10000, 10 由于MySQL里对LIMIT OFFSET的处理方式是取出OFFSET+LIMIT的所有数据,然后去掉OFFSET,返回底部的LIMIT。所以,在OFFSET数值较大时,MySQL的查询性能会非常低。可以使用id > n 的方式进行解决:使用id > n 的方式有局限性,对于id不连续的问题,可以通过翻页的时候同时传入最后一个id方式来解决。 123456http://example.com/page.php?last=100select * from table where id<100 order by id desc limit 10//上一页http://example.com/page.php?first=110select * from table where id>110 order by id desc limit 10 这种方式比较大的缺点是,如果在浏览中有插入/删除操作,翻页不会更新,而总页数可能仍然是根据新的count(*) 来计算,最终可能会产生某些记录访问不到。为了修补这个问题,可以继续引入当前页码以及在上次翻页以后是否有插入/删除等影响总记录数的操作并进行缓存。 1select * from table where id >= (select id from table order by id limit #offset#, 1) 拒绝大SQL,拆分成小SQL充分利用QUERY CACHE充分利用多核CPU使用in代替or,in的值不超过1000个禁止使用order by rand()使用EXPLAIN诊断,避免生成临时表 EXPLAIN语句(在MySQL客户端中执行)可以获得MySQL如何执行SELECT语句的信息。通过对SELECT语句执行EXPLAIN,可以知晓MySQL执行该SELECT语句时是否使用了索引、全表扫描、临时表、排序等信息。尽量避免MySQL进行全表扫描、使用临时表、排序等。详见官方文档。用union all而不是unionunion all与 union有什么区别?union和union all关键字都是将两个结果集合并为一个,但这两者从使用和效率上来说都有所不同。union在进行表链接后会筛选掉重复的记录,所以在表链接后会对所产生的结果集进行排序运算,删除重复的记录再返回结果。如: 12select * from test_union1union select * from test_union2 这个SQL在运行时先取出两个表的结果,再用排序空间进行排序删除重复的记录,最后返回结果集,如果表数据量大的话可能会导致用磁盘进行排序。而union all只是简单的将两个结果合并后就返回。这样,如果返回的两个结果集中有重复的数据,那么返回的结果集就会包含重复的数据了。从效率上说,union all要比union快很多,所以,如果可以确认合并的两个结果集中不包含重复的数据的话,那么就使用union all,如下: 1select * from test_union1 union all select * from test_union2 程序应有捕获SQL异常的处理机制禁止单条SQL语句同时更新多个表不使用select * ,SELECT语句只获取需要的字段消耗CPU和IO、消耗网络带宽无法使用覆盖索引减少表结构变更带来的影响因为大,select/join 可能生成临时表UPDATE、DELETE语句不使用LIMITINSERT语句必须显式的指明字段名称,不使用INSERT INTO table()INSERT语句使用batch提交(INSERT INTO table VALUES(),(),()……),values的个数不超过500统计表中记录数时使用COUNT(*),而不是COUNT(primary_key)和COUNT(1) 备注:仅针对Myisam数据更新建议使用二级索引先查询出主键,再根据主键进行数据更新禁止使用跨库查询禁止使用子查询,建议将子查询转换成关联查询针对varchar类型字段的程序处理,请验证用户输入,不要超出其预设的长度; 分表规范单表一到两年内数据量超过500w或数据容量超过10G考虑分表,需提前考虑历史数据迁移或应用自行删除历史数据,采用等量均衡分表或根据业务规则分表均可。要分表的数据表必须与DBA商量分表策略用HASH进行散表,表名后缀使用十进制数,下标从0开始按日期时间分表需符合YYYY[MM][dd][HH]格式采用合适的分库分表策略。例如千库十表、十库百表等禁止使用分区表,分区表对分区键有严格要,分区表在表变大后执行DDL、SHARDING、单表恢复等都变得更加困难。拆分大字段和访问频率低的字段,分离冷热数据 行为规范批量导入、导出数据必须提前通知DBA协助观察禁止在线上从库执行后台管理和统计类查询禁止有super权限的应用程序账号存在产品出现非数据库导致的故障时及时通知DBA协助排查推广活动或上线新功能必须提前通知DBA进行流量评估数据库数据丢失,及时联系DBA进行恢复对单表的多次alter操作必须合并为一次操作不在MySQL数据库中存放业务逻辑重大项目的数据库方案选型和设计必须提前通知DBA参与对特别重要的库表,提前与DBA沟通确定维护和备份优先级不在业务高峰期批量更新、查询数据库其他规范提交线上建表改表需求,必须详细注明所有相关SQL语句 其他规范日志类数据不建议存储在MySQL上,优先考虑Hbase或OceanBase,如需要存储请找DBA评估使用压缩表存储。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注 特别声明 原文作者:白程序员的自习室 本文原链:https://www.studytime.xin/article/mysql-internal-specifications.html 本文转载如有侵权,请联系站长删除,谢谢","tags":[{"name":"Mysql","slug":"Mysql","permalink":"http://damon008.github.io/tags/Mysql/"}]},{"title":"极客时间 | 买课奖励 | 返现 | 学习资料 | 学习路径 | 白嫖","date":"2020-07-22T00:03:01.000Z","path":"2020/07/22/popularize-01/","text":"极客时间 | 买课奖励 | 返现 | 学习资料 | 学习路径 | 白嫖 |最近几年,知识付费越来越火爆,花几十块钱订阅一个专栏,跟着讲师的课程安排一节节学习,学习结束后,就可以对一项技术或者一系列知识点有很好的掌握,渐渐地,越来越多的人接受了这种学习模式。相应的,知识付费平台也像雨后春笋般成长了起来。目前做的比较大的像知乎、得到、喜马拉雅 FM 等,平台上每门课程的订阅人数少则几千,多则几万,甚至几十万,像得到的课程《薛兆丰的经济学课》累计订阅人数已经超过 40 万。所以未来一段时间内,知识付费仍将是一种很受欢迎的学习模式。 极客时间是极客邦科技(InfoQ)出品的一个知识付费平台,他们的 slogan 是“提升你的技术认知”,相应地,他们的课程也大多数都是面向技术方向。现在平台上总共有七十多门课程,每个月也会有五、六门新课上线,合作讲师都是各个领域的专家和高级工程师,有着丰富的实践经验,同时,他们的课程价格平均只有几十块,特别实惠。 而【秃头和尚】是知识付费领域的导学导购平台,在平台上面,提供了极客时间所有课程的入口,通过我的平台前去极客时间购买课程,在课程购买成功后,极课助手会向你进行返现,返现额度普遍超过 30%,有些超过 40%,最高可返 36 元。而在新课上线时,极客时间会进行限时优惠活动,价格本身就已经远远低于原价,再加上和尚的返现,最终购买课程的花费基本低于课程原价的 50%,所以特别划算。如果你想提升自己,学习更多技能,同时享受低价优惠,可以关注【秃头和尚】公众号了解更多信息。愿你我一起努力,变成更好的我们。 通过我的链接买极客时间课程,极客时间官方会给我一些返现,有 ¥36、¥24、¥18等等,当然我会把这些返现给你们,不从中间赚取一分钱,只为回报支持我的粉丝! 同样道理你购买我分享的课程后,您分享后也可以得到 ¥36、¥24、¥18等等对应返现! 课程目录如下: 技术与商业案例解读AI技术内参左耳听风朱赟的技术管理课邱岳的产品手记人工智能基础课赵成的运维体系管理课推荐系统三十六式深入浅出区块链技术领导力实战笔记硅谷产品实战36讲从0开始学架构Java核心技术面试精讲微服务架构实战160讲趣谈网络协议从0开始学游戏开发机器学习40讲零基础学PythonReact实战进阶45讲软件测试52讲持续交付36讲快速上手Kotlin开发深入拆解Java虚拟机邱岳的产品实战程序员进阶攻略Go语言核心36讲技术管理实战36讲从0开始学微服务深入剖析Kubernetes数据结构与算法之美代码精进之路算法面试通关40讲白话法律42讲从0开始学大数据Nginx核心知识100讲MySQL实战45讲Linux性能优化实战Android开发高手课程序员的数学基础课玩转Git三剑客数据分析实战45讲10x程序员工作法TensorFlow快速入门与实战重学前端面试现场玩转Spring全家桶软件工程之美Java并发编程实战Go语言从入门到实战iOS开发高手课Vue开发实战趣谈Linux操作系统从0开始做增长许式伟的架构课大规模数据处理实战从0开发一款iOS App深入浅出计算机组成原理Web协议详解与抓包实战Python核心技术与实战深入拆解Tomcat & Jetty 零基础学JavaJava性能调优实战OpenResty从入门到实战透视HTTP协议玩转webpackKafka核心技术与实战SQL必知必会Linux实战技能100讲Elasticsearch核心技术与实战黄勇的OKR实战笔记Flutter核心技术与实战Spring Boot与Kubernetes云原生微服务实践 从0打造音视频直播系统TypeScript开发实战消息队列高手课网络编程实战浏览器工作原理与实践Swift核心技术与实战编译原理之美ZooKeeper实战与源码剖析研发效率破局之道即时消息技术剖析与实战全栈工程师修炼指南高并发系统设计40问Node.js开发实战分布式技术原理与算法解析说透中台DevOps实战笔记Netty源码剖析与实战DDD实战课苏杰的产品创新课移动端自动化测试实战项目管理实战20讲设计模式之美JavaScript核心原理解析MongoDB高手课后端技术面试38讲现代C++实战30讲性能工程高手课安全攻防技能30讲性能测试实战30讲小马哥讲Spring核心编程思想摄影入门课人人都能学会的编程入门课Electron开发实战说透敏捷.NET Core开发实战接口测试入门课分布式协议与算法实战RPC实战与核心原理架构实战案例解析NLP实战高手课后端存储实战课深入浅出云计算Java业务开发常见错误100例图解 Google V8SRE实战手册检索技术核心20讲数据中台实战课Service Mesh实战Kafka核心源码解读Serverless入门课视觉笔记入门课分布式缓存高手课系统性能调优必知必会罗剑锋的C++实战笔记互联网人的英语私教课职场求生攻略微信小程序全栈开发实战软件设计之美编译原理实战课TensorFlow 2项目进阶实战正则表达式入门课分布式系统案例课跟月影学可视化OAuth 2.0实战课Web安全攻防实战Selenium自动化测试实战Vim 实用技巧必知必会如何看懂一幅画 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"极客","slug":"极客","permalink":"http://damon008.github.io/tags/%E6%9E%81%E5%AE%A2/"}]},{"title":"基于OpenPAI细化部署 Hadoop 集群","date":"2020-07-18T06:34:34.000Z","path":"2020/07/18/hadoop-deploy/","text":"前提 https://github.com/microsoft/pai/tree/v0.14.0 Hadoop 2.9.0 k8s 1.9.4 (高版本未测) 本次讲解的主要是基于 Microsoft 开源的 OpenPAI,向大家通俗易懂的讲解 OpenPAI 是如何快速部署 Hadoop 集群的。便于大家快速部署Hadoop集群。 环境:1234ubuntu 16.04docker 18.06k8s 1.9.4Hadoop 2.9.0 1. 准备分析以上系统环境准备好,首先克隆 Microsoft 开源的 OpenPAI 的代码: https://github.com/microsoft/pai,切换到分支 v0.14.0。 由于我的目录在 /home/damon 下,所以直接: 12cd /home/damon/paill 可以看到有如下目录: 其中,src 目录下都是一些代码目录以及脚本: 我们再看看与 src 同一级的 deployment 目录: 看着很多,其实我们只要看 quick-start 下的几个文件: 1234sudo vi deployment/quick-start/quick-start-example.yaml #配置master节点信息sudo vi deployment/quick-start/kubernetes-configuration.yaml.template #不作大改sudo vi deployment/quick-start/layout.yaml.template #增加机器相关信息sudo vi deployment/quick-start/services-configuration.yaml.template #配置docker相关信息 第一个配置文件主要是关于 master 节点。第二个配置主要是配置 k8s 的基本信息,因为 OpenPAI 不仅可以部署 Hadoop,还可以基于 Docker、python 来部署 k8s。第三个配置主要是增加机器的信息,我们需要修改的是配置 master 节点的信息,至于 node 节点,我们可以通过打标签的方式来。第四个配置主要是配置 docker 信息,存储 image 的各种 tag 形式。 根据配置模板生成配置文件1sudo python paictl.py config generate -i deployment/quick-start/quick-start-example.yaml -o ~/damon/pai-config -f 把生成的本地配置文件推送到远程 k8s 集群1sudo python paictl.py config push -p ~/damon/pai-config/ 执行上面的命令时,会出现输入命令,意思是让你输入一个 cluster-id,这是 OpenPAI 为集群设置的一个 id。输入后回车即可把配置推送到远程了。 获取 cluster-id如果生成过,执行 12damon@master:~/damon/pai$ sudo python paictl.py config get-id2020-07-16 19:56:48,066 [INFO] - deployment.confStorage.get_cluster_id : Cluster-id is: ustc 即可获取。 重点以上配置都结束后,上面说过了,配置中只有 master 节点信息,需要手动给 node 节点打标签: 1kubectl label node nodeName hdfsrole=master 同样的标签还有类似: 123master labels:hdfsrole=master,jobhistory=true,launcher=true,node-exporter=true,pai-master=true,yarnrole=master,zookeeper=true 12node labels:gpu-check=true,hdfsrole=worker,node-exporter=true,pai-worker=true,yarnrole=worker 打完标签后,即可开始部署 Hadoop 集群了。 部署 Hadoop部署 Hadoop 的命令: 1sudo python paictl.py service start [-c /path/to/kubeconfig] [ -n service-name ] 解释:-c 参数中带的是 k8s 授权的 kube-config 路径,-n 参数是服务名,如果没带 -n,则会默认启动 src 下的所有的服务。 Hadoop 中主要有这些服务: 1234567zookeeperhadoop-name-nodehadoop-data-nodehadoop-resource-managerhadoop-node-managerhadoop-batch-jobhadoop-jobhistory 那就手动一个个执行吧。执行一个后看看 pod 有没有启动,相关的 configmap 有没有创建,默认都是官方的。 注意项如果发现 namenode 启用了安全模式,而不想启用的话,执行: 12kubectl exec -it hadoop-name-node-e3bw9 bashhadoop dfsadmin -safemode leave 即:进入 name-node 容器中执行关闭。 模块功能说明 resource-manager 是调度中心,负责资源管理。 node-manager 是容器启动的的执行者。通常异常情况需要重启 node-manager。 zookeeper 为数据的存储中心。 namenode 和 datanode 为 hadoop 服务(HDFS)的基础层。 模块运维方法说明 resource-manager 重启:大量任务 waiting 和 stopping 和数据不一致等情况。 node-manager 重启:更新节点的资源信息或者节点故障等。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"后端","slug":"后端","permalink":"http://damon008.github.io/tags/%E5%90%8E%E7%AB%AF/"}]},{"title":"ArrayList、LinkedList 你真的了解吗?","date":"2020-07-18T06:34:34.000Z","path":"2020/07/18/hello-world/","text":"1、 前言 经常在面试时,被问到集合的概念,集合 List、Map、Set 等底层设计以及其使用场景与注意细节。但大部分人的回答都是千篇一律,跟网上的答案一模一样,这是致命滴。其实,大家都错了,尤其是网上,更是误导大家,详细原因,且听我来分析。 2、集合 List 2.1 大家心中的 List在广大的网友心中,List 是一个缓存数据的容器,是 JDK 为开发者提供的一种集合类型。面试时,被问到最常见的就是 ArrayList 和 LinkedList 的区别。 相信大部分网友都能回答上:ArrayList 是基于数组实现,LinkedList 是基于链表实现。而在使用场景时,我发现大部分网友的答案都是:在新增、删除操作时,LinkedList 的效率要高于 ArrayList,而在查询、遍历操作的时候,ArrayList 的效率要高于 LinkedList。这个答案是否准确呢?今天就带大家验证一哈。 2.2 性能比较首先,大家都知道 ArrayList、LinkedList 都继承了 AbstractList 抽象类,而 AbstractList 实现了 List 接口。ArrayList 使用数组实现,而 LinkedList 使用了双向链表实现。接下来,我们就详细地分析下 ArrayList 和 LinkedList 的性能。 1234public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable 在源码中,我们知道 ArrayList 除了实现克隆和序列化,还实现了 RandomAccess 接口。大家可能会对这个接口比较陌生,通过代码我们可以发现,这个接口其实是一个空接口,没有实现逻辑,那么 ArrayList 为什么要实现它呢?原来 RandomAccess 接口是一个标志接口,它标志着只要实现该接口,就能实现快速随机访问。 至于 ArrayList、LinkedList 的各种操作方法这里不再说了,大家可以看 这一篇。 接下来,我们看看一些测试数据,以测试 50000 次为例: ArrayList、LinkedList 新增测试 123头部:ArrayList.Time 大于 LinkedList.Time中间:ArrayList.Time 小于 LinkedList.Time末尾:ArrayList.Time 小于 LinkedList.Time 通过这测试,我们可以看到 LinkedList 新增元素的未必要快于 ArrayList。 由于 ArrayList 是数组实现的,而数组是一块连续的内存空间,在新增元素到数组头部的时候,需要对头部以后的数据进行重排,所以效率很低。而 LinkedList 是基于链表实现,在新增元素的时候,首先会通过循环查找到新增元素的位置,如果要新增的位置处于前半段,就从前往后找;若其位置处于后半段,就从后往前找。故 LinkedList 新增元素到头部是非常高效的。 在中间位置插入时,ArrayList 同样有部分数据需要重排,效率也不是很高,而 LinkedList 将元素新增到中间,耗时最久的,因为靠近中间位置,在新增元素之前的循环查找是遍历元素最多的操作。 而在尾部操作时,发现在没有扩容的前提下,ArrayList 的效率要高于 LinkedList。这是因为 ArrayList 在新增元素到尾部的时候,不需要复制、重排,效率非常高。而 LinkedList 虽然也不用循环查找元素,但 LinkedList 中多了 new 对象以及变换指针指向对象的逻辑,所以要耗时多于 ArrayList 的操作。 12345678910111213141516 public boolean add(E e) { linkLast(e); return true;}void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++;} ArrayList、LinkedList 删除测试 123头部:ArrayList.Time 大于 LinkedList.Time中间:ArrayList.Time 小于 LinkedList.Time末尾:ArrayList.Time 小于 LinkedList.Time 大家会发现 ArrayList 和 LinkedList 删除操作的测试结果和新增的结果很接近,这是一样的道理,我就不赘述了。 ArrayList、LinkedList 遍历测试 12for循环:ArrayList.Time 小于 LinkedList.Time迭代器:ArrayList.Time 几乎等于 LinkedList.Time 我们可以看到,LinkedList 的 for 循环遍历比不上 ArrayList 的 for 循环。这是因为 LinkedList 基于链表实现的,在使用 for 循环的时候,每一次 for 循环都会去遍历大半个 List,所以严重影响了遍历的效率。而 ArrayList 是基于数组实现的,并且实现了 RandomAccess 接口标志,意味着 ArrayList 可以实现快速随机访问,所以 for 循环非常快。LinkedList 的迭代遍历和 ArrayList 的迭代性能差不多,也不会太差,所以在遍历 LinkedList 时,我们要使用迭代循环遍历。 3、常错点 思考在一次 ArrayList 删除操作的过程中,有下面两种写法: 12345678public static void removeA(ArrayList<String> l) { for (String s : l){ if (s.equals(\"aaa\")) { l.remove(s); } }} 1234567891011public static void removeB(ArrayList<String> l) { Iterator<String> it = l.iterator(); while (it.hasNext()) { String str = it.next(); if (str.equals(\"aaa\")) { it.remove(); } }} 第一种写法错误,第二种是正确的,原因是上面的两种写法都有用到 list 内部迭代器Iterator,即遍历时,ArrayList 内部创建了一个内部迭代器 iterator,在使用 next 方法来取下一个元素时,会使用 ArrayList 里保存的一个用来记录 list 修改次数的变量 modCount,与 iterator 保存了一个叫 expectedModCount 的表示期望的修改次数进行比较,如果不相等则会抛出一个叫 ConcurrentModificationException 的异常。且在 for 循环中调用 list 中的 remove 方法,会走到一个 fastRemove 方法,该方法不是 iterator 中的方法,而是 ArrayList 中的方法,在该方法只做了 modCount++,而没有同步到 expectedModCount。所以不一致就抛出了 ConcurrentModificationException 异常了。 下面是 ArrayList 自己的remove 方法: 12345678910111213141516public boolean remove(Object o) { if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false;} 12345678private void fastRemove(int index) { modCount++; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work} 如果有看过阿里 Java 编程规范就知道,在集合中进行 remove 操作时,不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: https://gitee.com/damon_one/spring-cloud-k8s 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"Java","slug":"Java","permalink":"http://damon008.github.io/tags/Java/"}]},{"title":"微服务自动化部署CI/CD","date":"2020-07-14T12:03:14.000Z","path":"2020/07/14/ci-cd/","text":"一直有人说想了解微服务的自动化部署,今天它来了。 在微服务化的时代,自动化部署越来越成为企业的重中之重了,因为这样减少了人员的成本,开发人员将代码提交后,触发相关事件即可部署测试环境,甚至得到许可后部署到线上。这样,原先开发人员、运维人员等要做的事,通通不必再重复劳作了。这对于一个企业来说,leader比较在乎的一件事。今天讲解通过jenkins、gitlab、harbor、k8s来作简单的CI/CD平台,暂时未涉及到代码检测等。 环境:123ubuntu16.04docker18.04k8s1.13.x + 1. 准备以上系统环境准备好,本文讲述的是用 k8s 来进行部署 jenkins 2. 部署 jenkins新建部署脚本 jenkins.yaml: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263apiVersion: v1kind: Servicemetadata: name: jenkins-service namespace: default labels: app: jenkinsspec: type: NodePort ports: - port: 8080 targetPort: 8080 protocol: TCP nodePort: 30600 name: jenkins - port: 30000 targetPort: 30000 nodePort: 30000 protocol: TCP name: agent selector: app: jenkins tier: jenkins---apiVersion: apps/v1kind: Deploymentmetadata: name: jenkins-deployment labels: app: jenkinsspec: replicas: 1 strategy: type: Recreate template: metadata: labels: app: jenkins tier: jenkins spec: containers: - name: jenkins image: jenkinsci/blueocean:latest imagePullPolicy: Always ports: - containerPort: 8080 name: jenkins - containerPort: 30000 name: agent volumeMounts: - mountPath: /var/jenkins_home name: jenkins-data - mountPath: /data/jenkins name: jenkins-log-path volumes: - name: jenkins-data hostPath: path: /home/demo/jenkins - name: jenkins-log-path hostPath: path: /data/jenkins 执行脚本: 1kubectl apply -f jenkins.yaml 查看部署成功的 pod 1kubectl get pod 同时会看到一个 service 生成,映射端口到外部。 接下来通过页面访问该服务,首次打开后,会让输入一个 admin 的密钥,这一串字符可以在日志中找到,执行 kubectl logs -f test-jenkins-c6bd58bf9-tgmsa 即可,输入一串字符后,进入下一步,需要安装一些插件,在安装插件的时候,需要注意的是,选择 jenkins 的 image 不同,其所需安装的插件也是不一样的,有的可能 image 已经存在了。具体请看:https://jenkins.io/zh/doc/book/installing/ 安装完插件后,进入下一步,创建第一个账户,管理员账户,一般建议创建,方便后面使用,注:邮箱也需要填写。 完成以上后,我们进入正式界面: 第一步:点击系统管理—>插件管理,添加一些插件,这里有用到kubernetes的插件,故安装了kubernetes plugins,其他的可根据自行项目确定下载、安装。 第二步:点击系统管理—>系统设置 添加jenkins的地址以及邮件地址 第三步:拉动滚动框到最下面,新增一个云 这里我加了kubernetes的配置,为什么后面会讲。 第四步:新建任务 在触发器中新增规则,最下面要生成token。 第五步: 接下来就是选择与gitlab互动,gitlab的地址以及凭据,凭据可通过首页加上可访问gitlab的用户信息,脚本路径需要根据自身的Jenkinsfile路径情况填写。 第六步: 如果启用全局安全,这个端口本身是50000,但由于k8s的默认nodePort范围是30000-32767,故可以修改在这区间内,比如:30000,这也是为什么上面创建时service的nodePort是30000了。 至此,jenkins环境配置完成 第七步:配置gitlab 这里的url就是在新建任务时生成的Gitlab webhook,token就是上面截图生成的token,最后add webhook。 第八步: 关于Harbor,自己可以简单搭建一个harbor服务,找度娘问一下很多,此处略。 第九步: 新建Jenkinsfile文件,如果刚才的路径是在项目根目录,则直接在项目根目录下创建Jenkinsfile文件: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960def label = \"mypod-${UUID.randomUUID().toString()}\"def k8sPodYaml = \"\"\" apiVersion: \"v1\" kind: \"Pod\" metadata: labels: jenkins: \"slave\" name: \"${label}\" spec: containers: - env: - name: \"CI_ENV\" value: \"YES\" - name: \"JENKINS_AGENT_WORKDIR\" value: \"/home/jenkins/agent\" image: \"jenkins/jnlp-slave\" imagePullPolicy: \"Always\" name: \"jnlp\" resources: limits: {} requests: {} securityContext: privileged: false tty: false volumeMounts: - mountPath: \"/home/jenkins/.kube\" name: \"volume-2\" readOnly: false - mountPath: \"/var/run/docker.sock\" name: \"volume-0\" readOnly: false - mountPath: \"/var/inference/config\" name: \"volume-1\" readOnly: false - mountPath: \"/usr/local/bin/kubectl\" name: \"volume-3\" readOnly: false - mountPath: \"/home/jenkins/agent\" name: \"workspace-volume\" readOnly: false workingDir: \"/home/jenkins/agent\" nodeSelector: {} restartPolicy: \"Never\" volumes: - hostPath: path: \"/var/run/docker.sock\" name: \"volume-0\" - hostPath: path: \"/home/leinao/.kube\" name: \"volume-2\" - hostPath: path: \"/home/leinao/inference-deploy/output_config\" name: \"volume-1\" - emptyDir: {} name: \"workspace-volume\" - hostPath: path: \"/usr/local/bin/kubectl\" name: \"volume-3\" 下面是自己的任务构建项目时的逻辑,由于我们这块用了自己的框架写的编译逻辑,故比较简单的,这里不合适公开,但是逻辑都差不多,大家可自行编写。 这块解释上面的k8s的yaml,这个就是为了在k8s中启动一个pod,通过该pod来执行构建逻辑的过程。 到此,关于Jenkins结合harbor、gitlab、k8s来实现CI/CD结束了,有几点注意的地方: 1. 如果jenkins是在容器中启动的一定要记得将这个端口(30000)暴露到外部,不然jenkins-master会不知道slave是否已经启动,会反复去创建pod只到超过重试次数。 2. 如果提示Jenkins doesn’t have label jenkins-jnlp-slave,可能原因: 1). 因为slave节点无法链接到jenkins节点开放端口50000导致 2). 因为slave镜像中slave启动失败导致的 3). 因为jenkins和k8s通信有延时导致超时jenkins会反复创建pod 4). 因为slave pod启动失败 5). 因为pipeline中指定的label与配置中的不一致导致 3. 也可以通过Ingress暴露的方式来进行暴露。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"自动化部署","slug":"自动化部署","permalink":"http://damon008.github.io/tags/%E8%87%AA%E5%8A%A8%E5%8C%96%E9%83%A8%E7%BD%B2/"}]},{"title":"Nginx 日常操作","date":"2020-07-14T07:05:15.000Z","path":"2020/07/14/nginx/","text":"Ubuntu 下 Nginx 安装 先下载相关文件: cd /usr/local/src sudo 模式下 wget http://nginx.org/download/nginx-1.10.2.tar.gz wget http://www.openssl.org/source/openssl-fips-2.0.10.tar.gz wget http://zlib.net/zlib-1.2.11.tar.gz wget https://netix.dl.sourceforge.net/project/pcre/pcre/8.40/pcre-8.40.tar.gz 安装nginx相关组件: 安装openssl sudo tar zxvf openssl-fips-2.0.10.tar.gz cd openssl-fips-2.0.10 sudo ./config && make && make install 安装pcre: sudo tar zxvf pcre-8.40.tar.gz cd pcre-8.40 sudo ./configure && make && make install 安装zlib: sudo tar zxvf zlib-1.2.11.tar.gz cd zlib-1.2.11 sudo ./configure && make && make install 安装nginx: sudo tar zxvf nginx-1.10.2.tar.gz cd nginx-1.10.2 sudo ./configure && make && make install 启动nginx: /usr/local/nginx/sbin/nginx 查看nginx是否启动成功: netstat -lnp 查看 nginx 的测试,以及相关配置: nginx -t nginx 默认的日志位置: tail -f /var/log/nginx/access.log 加配置: sudo /usr/local/nginx/sbin/nginx sudo /usr/local/nginx/sbin/nginx -s stop(quit、reload) sudo /usr/local/nginx/sbin/nginx -h sudo vi /usr/local/nginx/conf/nginx.conf Ubuntu 下 Nginx 完全卸载 先暂停 nginx 服务 1sudo service nginx stop 删除 nginx,–purge 包括配置文件: 1sudo apt-get --purge remove nginx 移除全部不使用的软件包: 1sudo apt-get autoremove 列出与 nginx 相关的软件 并删除显示的软件: 12345dpkg --get-selections|grep nginxsudo apt-get --purge remove nginxsudo apt-get --purge remove nginx-commonsudo apt-get --purge remove nginx-core 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"后端","slug":"后端","permalink":"http://damon008.github.io/tags/%E5%90%8E%E7%AB%AF/"}]},{"title":"Spring cloud 之熔断机制(实战)","date":"2020-07-10T06:16:14.000Z","path":"2020/07/10/springcloud-04/","text":"前面讲过 Spring cloud 之多种方式限流(实战)来处理请求频繁的压力。大家都知道,多个微服务之间调用的时候,假设微服务 A 调用微服务 B 和微服务 C,微服务 B 和微服务 C 有调用其他的微服务,这就是所谓的 扇出,若扇出的链路上某个微服务的请求时间过长或者不可用,对微服务 A 的调用就会占用越来越多的时间以及更多资源,进而引起系统雪崩,即”雪崩效应”。 这个时候,需要一个机制来保证当某个微服务出现异常时(请求反应慢或宕机),其整个流程还是阔以友好滴进行下去。即向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就可以保证调用方的线程不会被长时间、无厘头滴占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。我们把这个机制,或者这种处理方式叫作“熔断器”。 熔断机制是应对雪崩效应的一种微服务链路保护机制,当整个链路的某个微服务异常时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回“合理”的响应信息。当检测到该节点微服务正常后恢复调用链路,在Spring cloud 框架机制通过 Hystrix 实现,Hystrix 会监控微服务见调用的状况,当失败的调用到一个阈值,默认是5秒内20次调用失败就会启动熔断机制,熔断机制的注解是 @HystrixCommand。 最近研究了一下 Spring cloud 的熔断机制,特分享一些代码,以及实战中的坑。 在Spring cloud 中,假设有几个微服务:用户管理服务、订单服务、鉴权中心、物流服务等。这时,订单服务中,某个接口请求用户管理服务,这个时候如果需要熔断机制,该怎么处理呢? 首先,订单服务引入依赖: 12345678910<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> 这个时候,订单服务启动类中需要引用熔断注解 @EnableCircuitBreaker,使其生效: 12345678910111213141516171819/** * @author Damon * @date 2020年1月13日 下午3:23:06 * */@EnableOAuth2Sso@Configuration@EnableAutoConfiguration@ComponentScan(basePackages = {\"com.damon\"})@EnableDiscoveryClient@EnableCircuitBreakerpublic class OrderApp { public static void main(String[] args) { SpringApplication.run(OrderApp.class, args); }} 这里,不要忘记注解 @EnableDiscoveryClient 来相互暴露服务。 最后需要在调用用户管理服务的函数中,加入注解 @HystrixCommand: 123456789101112131415161718192021222324252627282930313233343536373839@HystrixCommand(fallbackMethod = \"admin_service_fallBack\", commandProperties = { @HystrixProperty(name = \"execution.isolation.thread.timeoutInMilliseconds\", value = \"5000\") })//隔离策略:execution.isolation.strategy =SEMAPHORE or THREAD(不配置默认) @Override public Response<Object> getUserInfo(HttpServletRequest req, HttpServletResponse res) { ResponseEntity<String> forEntity = restTemplate.getForEntity(envConfig.getAdmin_web_url() + \"/api/user/getUserInfo\", String.class); HttpHeaders headers = new HttpHeaders(); MediaType type = MediaType.parseMediaType(\"application/json; charset=UTF-8\"); headers.setContentType(type); headers.add(\"Accept\", MediaType.APPLICATION_JSON.toString()); headers.add(\"Authorization\", \"bearer \" + StrUtil.subAfter(req.getHeader(\"Authorization\"), \"bearer \", false)); HttpEntity<String> formEntity = new HttpEntity<String>(null, headers); String body = \"\"; try { ResponseEntity<String> responseEntity = restTemplate.exchange(\"http://admin-web-service/api/user/getUserInfo\", HttpMethod.GET, formEntity, String.class); if (responseEntity.getStatusCodeValue() == 200) { logger.debug(String.format(\"request getUserInfo return: {}\", JSON.toJSON(responseEntity.getBody()))); return Response.ok(responseEntity.getStatusCodeValue(), 0, \"success\", JSON.toJSON(responseEntity.getBody())); } } catch (Exception e) { logger.error(\"loadJobDetail error\"); logger.error(e.getMessage(), e); } return null; } /** * 熔断时调用的方法 * * 参数要与被请求的方法的参数一致 * * @return */ private Response<Object> admin_service_fallBack(HttpServletRequest req, HttpServletResponse res) { String token = StrUtil.subAfter(req.getHeader(\"Authorization\"), \"bearer \", false); logger.info(\"admin_service_fallBack token: {}\", token); return Response.ok(200, -2, \"用戶服務掛啦!\", null); } 其中上面代码需要注意的是:注解中 fallbackMethod 的值指定了熔断后的处理函数,这个函数的参数与当前的调用方法的参数需要保持一致,否则报错: 1com.netflix.hystrix.contrib.javanica.exception.FallbackDefinitionException:fallback method wasn't found. 最后,配置 Hystrix 相关的参数配置yaml: 12hystrix.command.BackendCall.execution.isolation.thread.timeoutInMilliseconds: 5000hystrix.threadpool.BackendCallThread.coreSize: 5 其中第一个配置在调用函数中其实也可以配置: 12@HystrixCommand(fallbackMethod = \"admin_service_fallBack\", commandProperties = { @HystrixProperty(name = \"execution.isolation.thread.timeoutInMilliseconds\", value = \"3000\") }) 这里配置的3000毫秒生效后,如果配置文件中也配置了,则会被覆盖。 如果不加@HystrixCommand中的commandProperties=@HystrixProperty注解配置,下面的FallBack函数admin_service_fallBack()是一个线程;@HystrixCommand()是一个隔离线程。若加上commandProperties=@HystrixProperty注解配置后,将2个线程合并到一个线程里。 这样到此为止,调用方就结束配置了,至于被调用方,相关配置与源码在Spring Cloud Kubernetes之实战二服务注册与发现&nbsp;一文中,讲过被调用服务的相关,这里的http://admin-web-service 为被调用服务,则在其服务启动类中需要注解 @EnableDiscoveryClient: 12345678910111213141516171819202122232425262728293031package com.damon;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.EnableAutoConfiguration;import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;import org.springframework.boot.context.properties.EnableConfigurationProperties;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import com.damon.config.EnvConfig;/** * @author Damon * @date 2020年1月13日 下午3:23:06 * */@EnableOAuth2Sso@Configuration@EnableAutoConfiguration@ComponentScan(basePackages = {\"com.damon\"})@EnableConfigurationProperties(EnvConfig.class)@EnableDiscoveryClientpublic class AdminApp { public static void main(String[] args) { SpringApplication.run(AdminApp.class, args); }} 另外,配置 RestTemplate 的 Bean 中加上注解 @LoadBalanced 需要作 LB,这样利用服务名来根据 LB 规则找到对应的其中一个服务,这样比较明显看出 LB 的效果: 12345678910111213141516171819202122232425262728293031package com.damon.config;import javax.annotation.Resource;import org.springframework.cloud.client.loadbalancer.LoadBalanced;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.env.Environment;import org.springframework.http.client.SimpleClientHttpRequestFactory;import org.springframework.web.client.RestTemplate;/** * @author Damon * @date 2018年2月2日 下午7:15:53 */@Configurationpublic class BeansConfig { @Resource private Environment env; @LoadBalanced//就不能用ip等形式来请求其他服务 @Bean public RestTemplate restTemplate() { SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); requestFactory.setReadTimeout(env.getProperty(\"client.http.request.readTimeout\", Integer.class, 15000)); requestFactory.setConnectTimeout(env.getProperty(\"client.http.request.connectTimeout\", Integer.class, 3000)); RestTemplate rt = new RestTemplate(requestFactory); return rt; }} 最后如果没问题了,可以先暂停用户管理服务,然后运行订单服务时,返回熔断结果: 1{\"message\":{\"code\":-2,\"message\":\"用戶服務掛啦!\",\"status\":200}} OK,Spring cloud 熔断实战就结束了! 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"Java","slug":"Java","permalink":"http://damon008.github.io/tags/Java/"}]},{"title":"Spring cloud 之多种方式限流(实战)","date":"2020-07-10T06:15:01.000Z","path":"2020/07/10/springcloud-03/","text":"在频繁的网络请求时,服务有时候也会受到很大的压力,尤其是那种网络攻击,非法的。这样的情形有时候需要作一些限制。例如:限制对方的请求,这种限制可以有几个依据:请求IP、用户唯一标识、请求的接口地址等等。 当前限流的方式也很多:Spring cloud 中在网关本身自带限流的一些功能,基于 redis 来做的。同时,阿里也开源了一款:限流神器 Sentinel。今天我们主要围绕这两块来实战微服务的限流机制。 首先讲 Spring cloud 原生的限流功能,因为限流可以是对每个服务进行限流,也可以对于网关统一作限流处理。 一、实战基于 Spring cloud Gateway 的限流 pom.xml引入依赖: 1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency> 其基础是基于redis,所以: 1234567891011121314spring: application: name: gateway-service redis: #redis相关配置 database: 8 host: 10.12.15.5 port: 6379 password: 123456 #有密码时设置 jedis: pool: max-active: 8 max-idle: 8 min-idle: 0 timeout: 10000ms 接下来需要注入限流策略的 bean: 1234567891011121314151617181920212223242526272829303132@Primary @Bean(value = \"ipKeyResolver\") KeyResolver ipKeyResolver() { return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName()); //return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()); //return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()); } /** * API限流 * @return * @author Damon * @date 2020年3月18日 * */ @Bean(value = \"apiKeyResolver\") KeyResolver apiKeyResolver() { return exchange -> Mono.just(exchange.getRequest().getPath().value()); } /** * 请求路径中必须携带userId参数 * 用户限流 * @return * @author Damon * @date 2020年3月18日 * */ @Bean(value = \"userKeyResolver\") KeyResolver userKeyResolver() { return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst(\"userId\")); } 这里引入ipKeyResolver、apiKeyResolver、userKeyResolver三种策略,可以利用注解 @Primary 来决定其中一个被使用。 注入bean后,需要在配置中备用: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576spring: application: name: gateway-service redis: #redis相关配置 database: 8 host: 10.12.15.5 port: 6379 password: 123456 #有密码时设置 jedis: pool: max-active: 8 max-idle: 8 min-idle: 0 timeout: 10000ms cloud: kubernetes: discovery: all-namespaces: true gateway: discovery: locator: enabled: true lowerCaseServiceId: true routes: #路由配置:参数为一个List - id: cas-server #唯一标识 uri: lb://cas-server-service #转发的地址,写服务名称 order: -1 predicates: - Path=/cas-server/** #判断匹配条件,即地址带有/ribbon/**的请求,会转发至lb:cas-server-service filters: - StripPrefix=1 #去掉Path前缀,参数为1代表去掉/ribbon - name: RequestRateLimiter #基于redis的Gateway的自身限流 args: redis-rate-limiter.replenishRate: 1 # 允许用户每秒处理多少个请求 redis-rate-limiter.burstCapacity: 3 # 令牌桶的容量,允许在一秒钟内完成的最大请求数 key-resolver: \"#{@ipKeyResolver}\" #SPEL表达式取的对应的bean - id: admin-web uri: lb://admin-web-service order: -1 predicates: - Path=/admin-web/** filters: - StripPrefix=1 - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 1 # 允许用户每秒处理多少个请求 redis-rate-limiter.burstCapacity: 3 # 令牌桶的容量,允许在一秒钟内完成的最大请求数 key-resolver: \"#{@ipKeyResolver}\" #SPEL表达式取的对应的bean - id: order-service uri: lb://order-service-service order: -1 predicates: - Path=/order-service/** filters: - StripPrefix=1 - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 1 # 允许用户每秒处理多少个请求 redis-rate-limiter.burstCapacity: 3 # 令牌桶的容量,允许在一秒钟内完成的最大请求数 key-resolver: \"#{@ipKeyResolver}\" #SPEL表达式取的对应的bean http: encoding: charset: UTF-8 enabled: true force: true mvc: throw-exception-if-no-handler-found: true main: allow-bean-definition-overriding: true # 当遇到同样名称时,是否允许覆盖注册 这里是在原有的路由基础上加入 RequestRateLimiter限流过滤器,包括三个参数: 12345- name: RequestRateLimiter #基于redis的Gateway的自身限流 args: redis-rate-limiter.replenishRate: 3 #允许用户每秒处理多少个请求 redis-rate-limiter.burstCapacity: 5 #令牌桶的容量,允许在一秒钟内完成的最大请求数 key-resolver: \"#{@ipKeyResolver}\" #SPEL表达式取的对应的bean 其中 replenishRate,其含义表示允许每秒处理请求数; burstCapacity 表示允许在一秒内处理的最大请求数; key-resolver 这里采用请求 IP 限流,利用SPEL 表达式取对应的 bean 写一个小脚本来压测一下: 123for i in $(seq 1 30000); do echo $(expr $i \\\\* 3 + 1);curl -i -H \"Accept: application/json\" -H \"Authorization:bearer b064d95b-af3f-4053-a980-377c63ab3413\" -X GET http://10.10.15.5:5556/order-service/api/order/getUserInfo;donefor i in $(seq 1 30000); do echo $(expr $i \\\\* 3 + 1);curl -i -H \"Accept: application/json\" -H \"Authorization:bearer b064d95b-af3f-4053-a980-377c63ab3413\" -X GET http://10.10.15.5:5556/admin-web/api/user/getCurrentUser;done 上面两个脚本分别对2个服务进行压测,打印结果: 12345678910111213141516171819202122232425262728293031323334HTTP/1.1 200 OKtransfer-encoding: chunkedX-RateLimit-Remaining: 2X-RateLimit-Burst-Capacity: 3X-RateLimit-Replenish-Rate: 1Expires: 0Cache-Control: no-cache, no-store, max-age=0, must-revalidateSet-Cookie: ORDER-SERVICE-SESSIONID=R99Ljit9XvfCapyUJDWL8I0rZqxReoY6HwcQV2n2; path=/X-XSS-Protection: 1; mode=blockPragma: no-cacheX-Frame-Options: DENYDate: Thu, 19 Mar 2020 06:32:27 GMTX-Content-Type-Options: nosniffContent-Type: application/json;charset=UTF-8{\"message\":{\"status\":200,\"code\":0,\"message\":\"success\"},\"data\":\"{\\\"message\\\":{\\\"status\\\":200,\\\"code\\\":0,\\\"message\\\":\\\"get user success\\\"},\\\"data\\\":{\\\"id\\\":23,\\\"isAdmin\\\":1,\\\"userId\\\":\\\"fbb18810-e980-428c-932f-848f3b9e7c84\\\",\\\"userType\\\":\\\"super_admin\\\",\\\"username\\\":\\\"admin\\\",\\\"realName\\\":\\\"super_admin\\\",\\\"password\\\":\\\"$2a$10$89AqlYKlnsTpNmWcCMvgluRFQ/6MLK1k/nkBpz.Lw6Exh.WMQFH6W\\\",\\\"phone\\\":null,\\\"email\\\":null,\\\"createBy\\\":\\\"admin\\\",\\\"createTime\\\":1573119753172,\\\"updateBy\\\":\\\"admin\\\",\\\"updateTime\\\":1573119753172,\\\"loginTime\\\":null,\\\"expireTime\\\":null,\\\"remarks\\\":\\\"super_admin\\\",\\\"delFlag\\\":0,\\\"loginType\\\":null}}\"}ex同一秒内多次后:HTTP/1.1 429 Too Many RequestsX-RateLimit-Remaining: 0X-RateLimit-Burst-Capacity: 3X-RateLimit-Replenish-Rate: 1content-length: 0expr: syntax errorHTTP/1.1 429 Too Many RequestsX-RateLimit-Remaining: 0X-RateLimit-Burst-Capacity: 3X-RateLimit-Replenish-Rate: 1content-length: 0expr: syntax error 从上面可以看到,执行后,会出现调用失败的情况,状态变为429 (Too Many Requests) 。 二、基于阿里开源限流神器:Sentinel 首先引入依赖: 12345<!--基于 阿里的sentinel作限流 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> 在配置文件 application.yaml 文件中配置,需要新增2个配置: 123456789101112131415161718spring: application: name: admin-web cloud: kubernetes: discovery: all-namespaces: true sentinel: eager: true #取消Sentinel控制台的懒加载 transport: dashboard: 10.12.15.2:8080 #sentinel的Dashboard地址 port: 8719 #是sentinel应用端和控制台通信端口 heartbeat-interval-ms: 500 #心跳时间 scg: fallback: #scg.fallback为sentinel限流后的响应配置 mode: response response-status: 455 response-body: 已被限流 其中,这里面配置了一个服务:spring.cloud.sentinel.transport.dashboard,配置的是 sentinel 的 Dashboard 地址。同时 spring.cloud.sentinel.transport.port 这个端口配置会在应用对应的机器上启动一个Http Server,该 Server 会与 Sentinel 控制台做交互。 Sentinel 默认为所有的 HTTP 服务提供限流埋点,上面配置完成后自动完成所有埋点,只需要控制配置限流规则即可。 这里我们讲下通过注解来给指定接口函数加上限流埋点,写一个RestController,在接口函数上加上注解 @SentinelResource: 123456789@GetMapping(value = \"/getToken\")@SentinelResource(\"getToken\")public Response<Object> getToken(Authentication authentication){ //Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); authentication.getCredentials(); OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails)authentication.getDetails(); String token = details.getTokenValue(); return Response.ok(200, 0, \"get token success\", token);} 以上代码部分完成了,接下来先安装SentinelDashBoard,Sentinel DashBoard下载地址:https://github.com/alibaba/Sentinel/releases 。 下载完成后,命令启动: 1java -jar sentinel-dashboard-1.6.2.jar 默认启动端口为8080,访问 IP:8080,就可以显示 Sentinel 的登录界面,用户名与密码均为sentinel。登录 Dashboard 成功后,多次访问接口”/getToken”,可以在 Dashboard 看到相应数据,这里不展示了。接下来可以设置接口的限流功能,在 “+流控” 按钮点击打开设置界面,设置阈值类型为 qps,单机阈值为5。 浏览器重复请求 http://10.10.15.5:5556/admin-web/api/user/getToken 如果超过阀值就会出现如下界面信息: 1Blocked by Sentinel (flow limiting) 此时,就看到Sentinel 限流起作用了,可以加上 spring.cloud.sentinel.scg.fallback 为sentinel 限流后的响应配置,亦可自定义限流异常信息: 1234567891011121314151617@GetMapping(value = \"/getToken\")@SentinelResource(value = \"getToken\", blockHandler = \"handleSentinelException\", blockHandlerClass = {MySentinelException.class}))public Response<Object> getToken(Authentication authentication){ //Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); authentication.getCredentials(); OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails)authentication.getDetails(); String token = details.getTokenValue(); return Response.ok(200, 0, \"get token success\", token);}public class MySentinelException { public static Response<Object> handleSentinelException(BlockException e) { Map<String,Object> map=new HashMap<>(); logger.info(\"Oops: \" + ex.getClass().getCanonicalName()); return Response.ok(200, -8, \"通过注解 @SentinelResource 配置限流埋点并自定义限流后的处理逻辑\", null); }} 这里讲下注解 @SentinelResource 包含以下属性: value:资源名称,必需项; entryType:入口类型,可选项(默认为 EntryType.OUT); blockHandler:blockHandlerClass中对应的异常处理方法名,参数类型和返回值必须和原方法一致; blockHandlerClass:自定义限流逻辑处理类 Sentinel 限流逻辑处理完毕了,但每次服务重启后,之前配置的限流规则就会被清空。因为是内存形式的规则对象。所以下面就讲下用 Sentinel 的一个特性 ReadableDataSource 获取文件、数据库或者配置中心设置限流规则,目前支持 Apollo、Nacos、ZK 配置来管理。 首先回忆一下,一条限流规则主要由下面几个因素组成: resource:资源名,即限流规则的作用对象,即为注解 @SentinelResource 的value; count:限流阈值;grade:限流阈值类型(QPS 或并发线程数); limitApp:流控针对的调用来源,若为 default 则不区分调用来源; strategy:基于调用关系的限流策略; controlBehavior:流量控制效果(直接拒绝、排队等待、匀速器模式) 理解了意思,接下来通过文件来配置: 1234#通过文件读取限流规则spring.cloud.sentinel.datasource.ds1.file.file=classpath:flowrule.jsonspring.cloud.sentinel.datasource.ds1.file.data-type=jsonspring.cloud.sentinel.datasource.ds1.file.rule-type=flow 在resources新建一个文件,比如 flowrule.json 添加限流规则: 123456789101112131415161718[ { \"resource\": \"getToken\", \"count\": 1, \"controlBehavior\": 0, \"grade\": 1, \"limitApp\": \"default\", \"strategy\": 0 }, { \"resource\": \"resource\", \"count\": 1, \"controlBehavior\": 0, \"grade\": 1, \"limitApp\": \"default\", \"strategy\": 0 }] 重新启动项目,出现如下日志说明成功: 12DataSource ds1-sentinel-file-datasource start to loadConfigDataSource ds1-sentinel-file-datasource load 2 FlowRule 如果采用 Nacos 作为配置获取限流规则,可在文件中加如下配置: 1234567891011121314151617181920spring: application: name: order-service cloud: nacos: config: server-addr: 10.10.15.5:8848 discovery: server-addr: 10.10.15.5:8848 sentinel: eager: true transport: dashboard: 10.10.15.5:8080 datasource: ds1: nacos: server-addr: 10.10.15.5:8848 dataId: ${spring.application.name}-flow-rules data-type: json rule-type: flow 以上即为限流的两种方式。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"Java","slug":"Java","permalink":"http://damon008.github.io/tags/Java/"}]},{"title":"面试被问finally 和 return,到底谁先执行?","date":"2020-07-10T06:13:40.000Z","path":"2020/07/10/core-java01/","text":"经常有人面试被问到,finally 和 return,到底谁先执行呢? 为了解决这个问题,其实我们可以先想想 finally 是被用来干嘛的呢?它是被用来结束一些正常的收尾动作或结束标识。也就是说无论怎么样,finally 都会被最后执行。例如:一般在操作数据库时,用Jdbc连接池连接数据库后释放资源,需要 finally 来处理。再如 redis 连接,在获取连接池处理完数据的增删改查后,需要释放其连接池。 但是,如果 return 是在 finally 前面呢?或者在 finally 后面呢?我们先来看看 return 在 finally 前面时,如: 123456789101112131415161718192021222324252627282930313233343536373839package com.test;/** * * * @author Damon * @date 2020年3月18日 上午11:02:08 * */public class App { public static void main(String[] args) { System.out.println(\"return result: \" + test()); } public static int test() { try { Thread.sleep(1); System.out.println(\"執行 return 1\"); return 1;// return 在try里,則先執行,再執行finally后才有可能执行该return } catch (InterruptedException e) { e.printStackTrace(); return -1; } finally { System.out.println(\"执行 finally\"); //return 3; } //System.out.println(\"執行 return 2\"); //return 1; }}结果:執行 return 1执行 finallyreturn result: 1 也就是说,在执行 return 之前,先执行了 finally。 我们在看,如果 finally 前面有 return,在其内部也有 return: 123456789101112131415161718192021222324252627282930313233343536373839package com.test;/** * * * @author Damon * @date 2020年3月18日 上午11:02:08 * */public class App { public static void main(String[] args) { System.out.println(\"return result: \" + test()); } public static int test() { try { Thread.sleep(1); System.out.println(\"執行 return 1\"); return 1;// return 在try里,則先執行,再執行finally后才有可能执行该return } catch (InterruptedException e) { e.printStackTrace(); return -1; } finally { System.out.println(\"执行 finally\"); return 3; } //System.out.println(\"執行 return 2\"); //return 1; }}结果:執行 return 1执行 finallyreturn result: 3 其内部被 return 后,就不再执行前面那个 return 了。 我们再来看 return 在 finally 之后,如: 123456789101112131415161718192021222324252627282930313233343536373839package com.test;/** * * * @author Damon * @date 2020年3月18日 上午11:02:08 * */public class App { public static void main(String[] args) { System.out.println(\"return result: \" + test()); } public static int test() { try { Thread.sleep(1); //System.out.println(\"執行 return 1\"); //return 1;// return 在try里,則先執行,再執行finally后才有可能执行该return } catch (InterruptedException e) { e.printStackTrace(); //return -1; } finally { System.out.println(\"执行 finally\"); //return 3; } System.out.println(\"執行 return 2\"); return 1; }}结果:执行 finally執行 return 2return result: 1 总结:finally 在 return 之后时,先执行 finally 后,再执行该 return;finally 内含有 return 时,直接执行其 return 后结束;finally 在 return 前,执行完 finally 后再执行 return。 接下来还有常被问到的是:Java 中 final、finally、finalize 的区别与用法: final 用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。即如果一个类被声明为 final,意味着它不能作为父类被继承,因此一个类不能同时被声明为 abstract 的,又被声明为 final 的。变量或方法被声明为 final,可以保证它们在使用中不被修改。被声明为 final 的变量必须在声明时给赋予初值,而在以后的引用中只能读取,不可修改。被声明为 final 的方法也同样只能使用,不能重载。 finally 是异常处理语句结构的一部分,总是执行,常见的场景:释放一些资源,例如前面所说的 redis、db 等。在异常处理时提供 finally 块来执行任何清除操作,即在执行 catch 后会执行 finally 代码块。 finalize 是 Object 类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"Java","slug":"Java","permalink":"http://damon008.github.io/tags/Java/"}]},{"title":"Spring Cloud Kubernetes之实战一配置管理","date":"2020-07-10T06:10:45.000Z","path":"2020/07/10/springcloud-02/","text":"一直以来,玩springcloud的,基本都是在玩Springboot1.x,Springcloud(Dalston版)的众多相关组件来做配置中心、服务注册与发现,网关用的是Netflix公司对springboot做的LB,等等,但是这些东西太过沉重,复杂了。在一个以k8s为基础的iaas服务系统,如果不用k8s的特性来做这些事,那是说不过去的。理由这就不重复述说了。一句话:减少系统服务的复杂性。 本文主要介绍springcloud结合k8s,做配置管理,避免更多服务组件的冗余,完美填坑版! 环境: ubuntu16.04 docker18.04 k8s1.13.x + maven3.5.3 java1.8 + springboot 2.1.1 spring-cloud-kubernetes:1.0.1.RELEAS 前提 Ubuntu下安装docker18.04 or 其它较高版本,k8s1.13.x及以上,jvm环境等。 创建项目基础依赖: 12345678910111213141516171819202122232425262728293031323334<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.8.RELEASE</version> <relativePath/> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <swagger.version>2.6.1</swagger.version> <xstream.version>1.4.7</xstream.version> <pageHelper.version>4.1.6</pageHelper.version> <fastjson.version>1.2.51</fastjson.version> <shiro.version>1.3.0</shiro.version> <!-- <kubernetes-client-version>6.0.1</kubernetes-client-version> --> <kubernetes-client-version>5.0.0</kubernetes-client-version> <fabric8-kubernetes-client.version>4.6.1</fabric8-kubernetes-client.version><!-- 对应k8s v1.15.3 --> <springcloud.version>Greenwich.SR4</springcloud.version> <springcloud.kubernetes.version>1.1.1.RELEASE</springcloud.kubernetes.version> <mysql.version>5.1.46</mysql.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${springcloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> 核心依赖: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-actuator-autoconfigure</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-kubernetes-config</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency><!-- springcloud-k8s-discovery --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-commons</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-kubernetes-core</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-kubernetes-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-kubernetes-ribbon</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> **本次依赖引入配置管理、服务的发现(即消费者)。** 如果有操作redis和db的话,引入相应的依赖: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253<!-- mybatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.1.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <!--分页插件--> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>${pageHelper.version}</version> </dependency> <!-- datasource pool--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.3</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.2</version> </dependency> <!-- 对redis支持,引入的话项目缓存就支持redis了,所以必须加上redis的相关配置,否则操作相关缓存会报异常 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-redis</artifactId> <version>1.4.7.RELEASE</version> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>19.0</version> </dependency> 剩下的就是构建镜像时的插件: 1234567891011121314151617181920212223242526<build> <finalName>${project.artifactId}</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <jvmArguments>-Dfile.encoding=UTF-8</jvmArguments> <fork>true</fork> </configuration> </plugin> <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.7.8</version> <executions> <execution> <goals> <goal>prepare-agent</goal> <goal>report</goal> </goals> </execution> </executions> </plugin> </plugins> </build> 接下来,我们创建主类: 12345678910@SpringBootApplication(scanBasePackages = { \"com.leinao\" })@EnableConfigurationProperties(EnvConfig.class)@EnableDiscoveryClient@EnableHystrix@EnableSchedulingpublic class AdminApp { public static void main(String[] args) { SpringApplication.run(AdminApp.class, args); }} 注意这里创建启动类时,对springboot的项目进行了优化,避免启动时加载很多,启动繁重,具体深度优化,可参考:https://mp.weixin.qq.com/s?__biz=MzU2NjIzNDk5NQ==&mid=2247487954&idx=1&sn=2426451f3bd83161cfe1237f82d6b448&key=f8fb043b3d2681a794e51a46e142af77355722dff712776af12b1f3c831218df6dfc329df63c8e5e550b3d88d58f0f178c4c3c16b141733e0e3344fa595e2bc25241d864d45132753fd99279b832de85&ascene=1&uin=MzQzMzI2NjAxMQ%3D%3D&devicetype=Windows+10&version=62070158&lang=zh_CN&pass_ticket=pnSSI9jAq0M11V5hYMmkoVm5qO%2FWk9l3UUUJMglbdtdDOzLHa7iHsDmwSzs486sD。 然后我们在进行配置,注意:据官方说,项目的src\\main\\resources路径下不要创建application.yml文件,只创建名为bootstrap.yml的文件: 123456789101112131415161718192021222324252627282930313233343536management: endpoint: restart: enabled: true health: enabled: true info: enabled: truespring: application: name: edge-admin cloud: kubernetes: config: sources: - name: ${spring.application.name} namespace: default discovery: all-namespaces: true reload: #自动更新配置的开关设置为打开 enabled: true #更新配置信息的模式:polling是主动拉取,event是事件通知 mode: polling #主动拉取的间隔时间是500毫秒 period: 500 http: encoding: charset: UTF-8 enabled: true force: true mvc: throw-exception-if-no-handler-found: true main: allow-bean-definition-overriding: true # 当遇到同样名称时,是否允许覆盖注册 这里,我创建了bootstrap文件,同时也加了application文件,启动时会先加载bootstrap,验证有效。 在application.yaml中,我们加入如下内容: 12345678910111213141516171819202122232425262728293031323334353637383940server: port: 9999 undertow: accesslog: enabled: false pattern: combined servlet: session: timeout: PT120Mlogging: path: /data/${spring.application.name}/logsmanagement: endpoint: restart: enabled: true health: enabled: true info: enabled: trueclient: http: request: connectTimeout: 8000 readTimeout: 30000mybatis: mapperLocations: classpath:mapper/*.xml typeAliasesPackage: com.demo.*.modelbackend: ribbon: eureka: enabled: false client: enabled: true ServerListRefreshInterval: 5000hystrix.command.BackendCall.execution.isolation.thread.timeoutInMilliseconds: 5000hystrix.threadpool.BackendCallThread.coreSize: 5 注意:这里的server设置session的超时时间,对于springboot2.0与1.0版本完全不一样了,具体看内容。 其他的application-test.yaml等配置文件,配置的是日志的级别: 1234567logging: level: com: leinao: INFO org: springframework: web: INFO 接下来配置环境配置: EnvConfig.java类作为环境变量配置,注解ConfigurationProperties的prefix=”spring_cloud”, 表示该类用到的配置项都是名为”spring_cloud”的配置项的子内容 : 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363package com.demo.config;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.context.annotation.Configuration;/** * 配置信息 * @author Damon * @date 2019年10月25日 下午1:54:01 * */@Configuration@ConfigurationProperties(prefix = \"greeting\")public class EnvConfig { private String message = \"This is a dummy message\"; private String container_command; private String model_dir_path; private String so_path; private String config_path; private String task_role_name; private String container_name; private String container_workdir; private String init_containers_image; private String service_account_name; private String spring_mq_host; private String spring_mq_port; private String spring_mq_user; private String spring_mq_pwd; private String jdbc_driverClassName; private String jdbc_url; private String jdbc_username; private String jdbc_password; private String spring_redis_host; private String spring_redis_port; private String spring_redis_pwd; private String kube_apiserver_address; private String image_path; private String volume_image_path; private String inference_job_namespace; private String api_version; private String remote_deployment_url; private String remote_pods_url; private String remote_deployment_pod_log_url; private String base_path; private String chunk_size; private String cas_url; private String create_job_url; private String abnormal_data_dir; private Long expire_time= 600000L; public String getMessage() { return this.message; } public void setMessage(String message) { this.message = message; } public String getContainer_command() { return container_command; } public void setContainer_command(String container_command) { this.container_command = container_command; } public String getModel_dir_path() { return model_dir_path; } public void setModel_dir_path(String model_dir_path) { this.model_dir_path = model_dir_path; } public String getSo_path() { return so_path; } public void setSo_path(String so_path) { this.so_path = so_path; } public String getConfig_path() { return config_path; } public void setConfig_path(String config_path) { this.config_path = config_path; } public String getTask_role_name() { return task_role_name; } public void setTask_role_name(String task_role_name) { this.task_role_name = task_role_name; } public String getContainer_name() { return container_name; } public void setContainer_name(String container_name) { this.container_name = container_name; } public String getContainer_workdir() { return container_workdir; } public void setContainer_workdir(String container_workdir) { this.container_workdir = container_workdir; } public String getInit_containers_image() { return init_containers_image; } public void setInit_containers_image(String init_containers_image) { this.init_containers_image = init_containers_image; } public String getService_account_name() { return service_account_name; } public void setService_account_name(String service_account_name) { this.service_account_name = service_account_name; } public String getSpring_mq_host() { return spring_mq_host; } public void setSpring_mq_host(String spring_mq_host) { this.spring_mq_host = spring_mq_host; } public String getSpring_mq_port() { return spring_mq_port; } public void setSpring_mq_port(String spring_mq_port) { this.spring_mq_port = spring_mq_port; } public String getSpring_mq_user() { return spring_mq_user; } public void setSpring_mq_user(String spring_mq_user) { this.spring_mq_user = spring_mq_user; } public String getSpring_mq_pwd() { return spring_mq_pwd; } public void setSpring_mq_pwd(String spring_mq_pwd) { this.spring_mq_pwd = spring_mq_pwd; } public String getJdbc_driverClassName() { return jdbc_driverClassName; } public void setJdbc_driverClassName(String jdbc_driverClassName) { this.jdbc_driverClassName = jdbc_driverClassName; } public String getJdbc_url() { return jdbc_url; } public void setJdbc_url(String jdbc_url) { this.jdbc_url = jdbc_url; } public String getJdbc_username() { return jdbc_username; } public void setJdbc_username(String jdbc_username) { this.jdbc_username = jdbc_username; } public String getJdbc_password() { return jdbc_password; } public void setJdbc_password(String jdbc_password) { this.jdbc_password = jdbc_password; } public String getSpring_redis_host() { return spring_redis_host; } public void setSpring_redis_host(String spring_redis_host) { this.spring_redis_host = spring_redis_host; } public String getSpring_redis_port() { return spring_redis_port; } public void setSpring_redis_port(String spring_redis_port) { this.spring_redis_port = spring_redis_port; } public String getSpring_redis_pwd() { return spring_redis_pwd; } public void setSpring_redis_pwd(String spring_redis_pwd) { this.spring_redis_pwd = spring_redis_pwd; } public String getKube_apiserver_address() { return kube_apiserver_address; } public void setKube_apiserver_address(String kube_apiserver_address) { this.kube_apiserver_address = kube_apiserver_address; } public String getImage_path() { return image_path; } public void setImage_path(String image_path) { this.image_path = image_path; } public String getVolume_image_path() { return volume_image_path; } public void setVolume_image_path(String volume_image_path) { this.volume_image_path = volume_image_path; } public String getInference_job_namespace() { return inference_job_namespace; } public void setInference_job_namespace(String inference_job_namespace) { this.inference_job_namespace = inference_job_namespace; } public String getApi_version() { return api_version; } public void setApi_version(String api_version) { this.api_version = api_version; } public String getRemote_deployment_url() { return remote_deployment_url; } public void setRemote_deployment_url(String remote_deployment_url) { this.remote_deployment_url = remote_deployment_url; } public String getRemote_pods_url() { return remote_pods_url; } public void setRemote_pods_url(String remote_pods_url) { this.remote_pods_url = remote_pods_url; } public String getRemote_deployment_pod_log_url() { return remote_deployment_pod_log_url; } public void setRemote_deployment_pod_log_url(String remote_deployment_pod_log_url) { this.remote_deployment_pod_log_url = remote_deployment_pod_log_url; } public String getBase_path() { return base_path; } public void setBase_path(String base_path) { this.base_path = base_path; } public String getChunk_size() { return chunk_size; } public void setChunk_size(String chunk_size) { this.chunk_size = chunk_size; } public Long getExpire_time() { return expire_time; } public void setExpire_time(Long expire_time) { this.expire_time = expire_time; } public String getCas_url() { return cas_url; } public void setCas_url(String cas_url) { this.cas_url = cas_url; } public String getCreate_job_url() { return create_job_url; } public void setCreate_job_url(String create_job_url) { this.create_job_url = create_job_url; } public String getAbnormal_data_dir() { return abnormal_data_dir; } public void setAbnormal_data_dir(String abnormal_data_dir) { this.abnormal_data_dir = abnormal_data_dir; }}测试demo类:/** * @author Damon * @date 2019年12月27日 上午9:16:41 * */@RestControllerpublic class DemoController { @Autowired private EnvConfig envConfig; /** * * @author Damon * @date 2019年12月26日 * */ @GetMapping(value = \"/getTest\") public String getTest() { return envConfig.getBase_path(); }} 重点:默认的svc是没有权限访问k8s的API Server的资源的,执行如下脚本,可以提升权限,允许其访问configmap的可读权限: 123456789101112131415#使用这个代表集群最高权限,deployment中无需引入serviceAccount: config-readerapiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: name: fabric8-rbacsubjects: - kind: ServiceAccount # Reference to upper's `metadata.name` name: default # Reference to upper's `metadata.namespace` namespace: defaultroleRef: kind: ClusterRole name: cluster-admin apiGroup: rbac.authorization.k8s.io 配置configmap: 1234567891011121314151617181920212223kind: ConfigMapapiVersion: v1metadata: name: edge-admindata: application.yaml: |- greeting: message: Say Hello to the World --- spring: profiles: dev greeting: message: Say Hello to the Developers --- spring: profiles: test greeting: message: Say Hello to the Test --- spring: profiles: prod greeting: message: Say Hello to the Prod 接下来就是执行deployment启动项目了: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162apiVersion: apps/v1kind: Deploymentmetadata: name: edge-admin-deployment labels: app: edge-adminspec: replicas: 1 selector: matchLabels: app: edge-admin template: metadata: labels: app: edge-admin spec: nodeSelector: edge-admin: \"true\" containers: - name: edge-admin image: 10.11.2.20:8000/harbor/edge-admin imagePullPolicy: IfNotPresent ports: - name: admin01 containerPort: 1002 volumeMounts: - mountPath: /home/edge-admin name: edge-admin-path - mountPath: /data/edge-admin name: edge-admin-log-path - mountPath: /etc/kubernetes name: kube-config-path - mountPath: /abnormal_data_dir name: abnormal-data-dir args: [\"sh\", \"-c\", \"nohup java $JAVA_OPTS -jar -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m -Xms1024m -Xmx1024m -Xmn256m -Xss256k -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC edge-admin.jar --spring.profiles.active=dev\", \"&\"] hostAliases: - ip: \"10.10.1.5\" hostnames: - \"k8s.api.server\" - \"foo.remote\" - ip: \"127.0.0.1\" hostnames: - \"foo.localhost\" - ip: \"0.0.0.0\" hostnames: - \"foo.all\" #利用admin-rbac.yaml来获取权限 #serviceAccount: config-reader #serviceAccountName: config-reader volumes: - name: edge-admin-path hostPath: path: /var/pai/edge-admin - name: edge-admin-log-path hostPath: path: /data/edge-admin - name: kube-config-path hostPath: path: /etc/kubernetes - name: abnormal-data-dir hostPath: path: /data/images/detect_result/defect 其中,前面说的,项目启动参数对其性能优化,是对jvm的参数设置。分别执行kubectl apply -f deployment.yaml和configmap.yaml,创建demo时所用的configmap的资源以及利用k8s部署启动项目。 最后打开浏览器:执行ip:port/hello,即可看到configmap中对应的属性值,这里就不展示了,有兴趣的可以试试。 以上即是对springcloud和k8s首次结合后利用其configmap特性,来做配置管理,摒弃springcloud-config、spring-boot-starter-actuator的组件,减少系统的复杂性,毕竟k8s是肯定会被用到的,所以可以直接用其特性来做系统服务的环境配置管理。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: 123https://gitee.com/damon_one/spring-cloud-k8shttps://gitee.com/damon_one/spring-cloud-oauth2https://gitee.com/damon_one/Springcloud-Learning-Dalston 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"Java","slug":"Java","permalink":"http://damon008.github.io/tags/Java/"}]},{"title":"浅谈负载均衡","date":"2020-07-10T05:47:15.000Z","path":"2020/07/10/springcloud-01/","text":"1、 前言 负载均衡,英文:Load Balance,其含义是请求分发到多个粒度单元上进行执行操作,例如各种服务器、应用服务、中台服务、数据服务等,从而达到共同完成某项任务的目的。为了拓宽网络设备和服务器的带宽、增加吞吐量、加强网络请求处理能力、提高网络的灵活性和高可用性,负载均衡是一种廉价、有效、透明的方法,它为服务的高并发做了一次缓冲,让单个服务的压力瞬间减少,实现了服务的高可用,避免服务因为压力而面临宕机的危险。 2、负载均衡 2.1 基于网络的负载均衡大家都知道,OSI 模型有 7 层结构,每层都可以有几个子层。OSI 的 7 层从上到下分别是物理层、数据链路层、网络层、传输层、会话层、表示层、应用层: 在这七层结构中,高层次都是依赖于低层次的。层次越高,使用起来越方便。 根据负载均衡技术实现在 OSI 七层模型的不同层次,是可以给负载均衡分类的。 常见的实现方式中,主要可以在应用层、传输层、网络层和数据传输层做文章。所以,工作在应用层的负载均衡,我们通常称之为七层负载均衡、工作在传输层的我们称之为四层负载均衡。我们一个个来看看: 七层负载均衡 七层负载均衡工作在 OSI 模型的应用层,应用层协议较多,常用 http、dns、ftp 等。七层负载就可以基于这些协议来负载。这些应用层协议中会包含很多有意义的内容。比如同一个 Web 服务器的负载均衡,除了根据 IP 加 port 进行负载外,还可根据 URL 来决定是否要进行负载均衡。 四层负载均衡 四层负载均衡工作在 OSI 模型的传输层,由于在传输层,只有 TCP/UDP 协议,这两种协议中除了包含源 IP、目标 IP 以外,还包含源端口及目的端口。四层负载均衡服务器在接受到客户端请求后,以后通过修改数据包的地址信息(IP+端口号)将流量转发到应用服务器。 2.2 负载均衡工具负载均衡的工具,常见的有 Nginx、k8s、Ribbon、Feign、HAProxy 等。 Nginx Nginx 主要用来作七层负载均衡,反向代理 http、https 的协议链接,同时也提供了 IMAP/POP3/SMTP 的服务。 upstream proxy_demo_aaa { server weight=5; server weight=6;} location ~ ^/demo-aaa/api(.*)$ { proxy_pass http://proxy_demo_aaa/api$1$is_args$args;}k8s k8s 的负载均衡是基于 kube-proxy,其服务发现基于 kube-dns,最后由于每个 Service 对应的 pod 可以是多个,所以可以基于 kube-proxy 实现负载均衡,kube-proxy 进程其实就是一个智能的软件负载均衡器,他负责把 service 的请求转发到后端的某个 pod 实例。 Ribbon Ribbon 是一个为客户端提供负载均衡功能的服务,它内部提供了一个叫做 ILoadBalance 的接口代表负载均衡器的操作,比如有添加服务器、选择服务器、获取所有的服务器列表、获取可用的服务器列表等等。 常见的,使用 RestTemplate 进行服务提供者、服务消费者之间的通信,只需为 RestTemplate 配置类添加@LoadBalanced 注解即可。 @Bean@LoadBalanced public RestTemplate restTemplate() { return new RestTemplate();}Feign Feign 是一个声明式负载均衡客户端使用 Feign 能让编写 WebService 的客户端更加简单,它的使用方法是定义一个接口,然后在上面添加注解,同时也支持 JAX-RS 标准的注解。Feign 也支持可拔插式的编码器和解码器。 @FeignClient(name = “provider-service”, configuration = {Feign4HttpConfiguration.class, FeignLogConfiguration.class}, fallback = CustomerClientImpl.class)public interface CustomerClient { @PostMapping(\"/save\") String save(); @GetMapping(\"/api/user/getUserInfo\") Response<Object> getUserInfo(); }HAProxy HAProxy 是一个使用 C 语言编写的自由、开放源代码软件,其提供高可用性、负载均衡,以及基于 TCP 和 HTTP 的应用程序代理的功能。 2.3 负载均衡算法 常见的几种负载均衡的算法有:随机、轮询、最少链接、Hash、加权、重试等。 随机:即请求随机分配到各台服务器上,这是默认的策略机制。 轮询:将所有请求,依次分发到每台服务器上,适合服务器硬件相同的场景,服务请求数相同。 最少链接:将本次请求分配到请求数最少的服务上,这种可以根据服务器当前的请求处理情况,动态分配。 Hash:根据 IP 地址进行 Hash 计算,得到 IP 地址,这种可以将来自同一 IP 地址的请求,同一会话期内,转发到同一服务器;实现会话粘滞。但目标服务器宕机后,会话也会随之丢失。 加权:在上面几种算法基础上,进行一定的加权比例分配。 重试:这种策略一般都会有,就是在调用失败后,进行二次重试机制。 当然,还有其他的动态的算法规则:最快模式、观察模式、动态性能分配等。 结束福利 开源实战利用 k8s 作微服务的架构设计代码: https://gitee.com/damon_one/spring-cloud-k8s 欢迎大家 star,多多指教。 关于作者 笔名:Damon,技术爱好者,长期从事 Java 开发、Spring Cloud 的微服务架构设计,以及结合 Docker、K8s 做微服务容器化,自动化部署等一站式项目部署、落地。目前主要从事基于 K8s 云原生架构研发的工作。Golang 语言开发,长期研究边缘计算框架 KubeEdge、调度框架 Volcano 等。公众号 交个朋友之猿天地 发起人。个人微信 DamonStatham,星球:《交个朋友之猿田地》,个人网站:交个朋友之猿天地 | 微服务 | 容器化 | 自动化,欢迎來撩。 欢迎关注:InfoQ 欢迎关注:腾讯自媒体专栏 欢迎关注","tags":[{"name":"后端","slug":"后端","permalink":"http://damon008.github.io/tags/%E5%90%8E%E7%AB%AF/"}]}]