首页 文章 精选 留言 我的

精选列表

搜索[高DPI],共10007篇文章
优秀的个人博客,低调大师

2024年社区高光时刻总结!

亲爱的朋友们,让我们把时间的快门按下,一起回顾这一年的点点滴滴。就像翻阅一本精彩的相册,每一张照片都记录着我们Apache DolphinScheduler社区的欢笑和泪水。那么,准备好了吗?让我们一起跳进时光机,回顾社区里2024年那些难忘的瞬间吧! 时光荏苒,新年将至,2025年,愿我们所有人的梦想照进现实,愿所有愿美好如期而至! 本文由 白鲸开源科技 提供发布支持!

优秀的个人博客,低调大师

使用 KubeSphere 部署高可用 RocketMQ 集群

作者:老Z,云原生爱好者,目前专注于云原生运维,KubeSphere Ambassador。 Spring Cloud Alibaba 全家桶之 RocketMQ 是一款典型的分布式架构下的消息中间件产品,使用异步通信方式和发布订阅的消息传输模型。 很多基于 Spring Cloud 开发的项目都喜欢采用 RocketMQ 作为消息中间件。 RocketMQ 常用的部署模式如下: 单 Master 模式 多 Master 无 Slave 模式 多 Master 多 Slave 模式-异步复制 多 Master 多 Slave 模式-同步双写 更多的部署方案详细信息可以参考官方文档。 本文重点介绍 单 Master 模式和多 Master 多 Slave-异步复制模式在 K8s 集群上的部署方案。 单 Master 模式 这种部署方式风险较大,仅部署一个 NameServer 和一个 Broker,一旦 Broker 重启或者宕机时,会导致整个服务不可用,不建议线上生产环境使用,仅可以用于开发和测试环境。 部署方案参考官方rocketmq-docker项目中使用的容器化部署方案涉及的镜像、启动方式、定制化配置。 多 Master 多 Slave-异步复制模式 每个 Master 配置一个 Slave,有多对 Master-Slave,HA 采用异步复制方式,主备有短暂消息延迟(毫秒级),这种模式的优缺点如下: 优点:即使磁盘损坏,消息丢失的非常少,且消息实时性不会受影响,同时 Master 宕机后,消费者仍然可以从 Slave 消费,而且此过程对应用透明,不需要人工干预,性能同多 Master 模式几乎一样; 缺点:Master 宕机,磁盘损坏情况下会丢失少量消息。 多 Master 多 Slave-异步复制模式适用于生产环境,部署方案采用官方提供的 RocketMQ Operator。 离线镜像制作 此过程为可选项,离线内网环境可用,如果不配置内网镜像,后续的资源配置清单中注意容器的 image 参数请使用默认值。 本文分别介绍了单 Master 模式和多 Master 多 Slave-异步复制模式部署 RocketMQ 使用的离线镜像的制作方式。 单 Master 模式直接采用 RocketMQ 官方文档中介绍的容器化部署方案中使用的镜像。 多 Master 多 Slave-异步复制模式的离线镜像制作方式采用 RocketMQ Operator 官方自带的镜像制作工具制作打包,制作过程中很多包都需要到国外网络下载,但是受限于国外网络访问,默认成功率较低,需要多次尝试或采取特殊手段 ( 懂的都懂)。 也可以用传统的方式手工的 Pull Docker Hub 上已有的镜像,然后再 Push 到私有镜像仓库。 在一台能同时访问互联网和内网 Harbor 仓库的服务器上进行下面的操作。 在 Harbor 中创建项目 本人习惯内网离线镜像的命名空间跟应用镜像默认的命名空间保持一致,因此,在 Harbor 中创建 apache 和 apacherocketmq 两个项目,可以在 Harbor 管理界面中手工创建项目,也可以用下面命令行的方式自动化创建。 curl -u "admin:Harbor12345" -X POST -H "Content-Type: application/json" https://registry.zdevops.com.cn/api/v2.0/projects -d '{ "project_name": "apache", "public": true}' curl -u "admin:Harbor12345" -X POST -H "Content-Type: application/json" https://registry.zdevops.com.cn/api/v2.0/projects -d '{ "project_name": "apacherocketmq", "public": true}' 安装 Go 1.16 RocketMQ Operator 自定义镜像制作需要用到 Go 环境,需要先安装配置。 下载 Go 1.16 系列的最新版: cd /opt/ wget https://golang.google.cn/dl/go1.16.15.linux-amd64.tar.gz 解压源代码到指定目录: tar zxvf go1.16.15.linux-amd64.tar.gz -C /usr/local/ 配置环境变量: cat >> /etc/profile.d/go.sh << EOF # go environment export GOROOT=/usr/local/go export GOPATH=/srv/go export PATH=$PATH:$GOROOT/bin:$GOPATH/bin EOF GOPATH 为工作目录也是代码的存放目录,可以根据自己的习惯配置 配置 Go: go env -w GO111MODULE=on go env -w GOPROXY=https://goproxy.cn,direct 验证: source /etc/profile.d/go.sh go verison 获取 RocketMQ Operator 从 Apache 官方 GitHub 仓库获取 rocketmq-operator 代码。 cd /srv git clone -b 0.3.0 https://github.com/apache/rocketmq-operator.git 制作 RocketMQ Operator Image 修改 DockerFile: cd /srv/rocketmq-operator vi Dockerfile Notice: 构建镜像的过程需访问国外的软件源和镜像仓库,在国内访问有时会受限制,因此可以提前修改为国内的软件源和镜像仓库。 此操作为可选项,如果访问不受限则不需要配置。 必要的修改内容: # 第 10 行(修改代理地址为国内地址,加速访问) # 修改前 RUN go mod download # 修改后 RUN go env -w GOPROXY=https://goproxy.cn,direct && go mod download # 第 25 行(修改源地址为国内源) # 修改前 RUN apk add --no-cache bash gettext nmap-ncat openssl busybox-extras # 修改后 RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories && \ apk add --no-cache bash gettext nmap-ncat openssl busybox-extras 可选的修改内容: # 默认安装的 ROCKETMQ版本为 4.9.4,可以修改为指定版本 # 第 28 行,修改 4.9.4 ENV ROCKETMQ_VERSION 4.9.4 制作镜像: yum install gcc cd /srv/rocketmq-operator go mod tidy IMAGE_URL=registry.zdevops.com.cn/apacherocketmq/rocketmq-operator:0.3.0 make docker-build IMG=${IMAGE_URL} 验证镜像构建成功: docker images | grep rocketmq-operator 推送镜像: make docker-push IMG=${IMAGE_URL} 清理临时镜像 docker rmi registry.zdevops.com.cn/apacherocketmq/rocketmq-operator:0.3.0 制作 RocketMQ Broker Image 修改 DockerFile(可选): cd /srv/rocketmq-operator/images/broker/alpine vi Dockerfile 此操作为可选项,主要是为了安装软件加速,如果访问不受限则不需要配置。 # 第 20 行(修改源地址为国内源) # 修改前 RUN apk add --no-cache bash gettext nmap-ncat openssl busybox-extras # 修改后 RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories && \ apk add --no-cache bash gettext nmap-ncat openssl busybox-extras 修改镜像构建脚本: # 修改镜像仓库地址为内网地址 sed -i 's#apacherocketmq#registry.zdevops.com.cn/apacherocketmq#g' build-broker-image.sh 构建并推送镜像: ./build-broker-image.sh 4.9.4 验证镜像构建成功: docker images | grep rocketmq-broker 清理临时镜像: docker rmi registry.zdevops.com.cn/apacherocketmq/rocketmq-broker:4.9.4-alpine-operator-0.3.0 制作 RocketMQ Name Server Image 修改 DockerFile(可选): cd /srv/rocketmq-operator/images/namesrv/alpine vi Dockerfile 此操作为可选项,主要是为了安装软件加速,如果访问不受限则不需要配置。 # 第 20 行(修改源地址为国内源) # 修改前 RUN apk add --no-cache bash gettext nmap-ncat openssl busybox-extras # 修改后 RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories && \ apk add --no-cache bash gettext nmap-ncat openssl busybox-extras 修改镜像构建脚本: # 修改镜像仓库地址为内网地址 sed -i 's#apacherocketmq#registry.zdevops.com.cn/apacherocketmq#g' build-namesrv-image.sh 构建并推送镜像: ./build-namesrv-image.sh 4.9.4 验证镜像构建成功: docker images | grep rocketmq-nameserver 清理临时镜像: docker rmi registry.zdevops.com.cn/apacherocketmq/rocketmq-nameserver:4.9.4-alpine-operator-0.3.0 根据官方已有镜像制作离线镜像 上面的 RocketMQ 多 Master 多 Slave-异步复制模式部署方案中用到的离线镜像制作方案更适合于本地修改定制的场景,如果单纯的只想把官方已有镜像不做修改的下载并推送到本地仓库,可以参考下面的方案。 下载镜像: docker pull apache/rocketmq-operator:0.3.0 docker pull apacherocketmq/rocketmq-nameserver:4.5.0-alpine-operator-0.3.0 docker pull apacherocketmq/rocketmq-broker:4.5.0-alpine-operator-0.3.0 Notice: 官方仓库最新版的镜像是 2 年前的 4.5.0. 重新打 tag: docker tag apache/rocketmq-operator:0.3.0-snapshot registry.zdevops.com.cn/apacherocketmq/rocketmq-operator:0.3.0 docker tag apacherocketmq/rocketmq-nameserver:4.5.0-alpine-operator-0.3.0 registry.zdevops.com.cn/apacherocketmq/rocketmq-nameserver:4.5.0-alpine-operator-0.3.0 docker tag apacherocketmq/rocketmq-broker:4.5.0-alpine-operator-0.3.0 registry.zdevops.com.cn/apacherocketmq/rocketmq-broker:4.5.0-alpine-operator-0.3.0 推送到私有镜像仓库: docker push registry.zdevops.com.cn/apacherocketmq/rocketmq-operator:0.3.0 docker push registry.zdevops.com.cn/apacherocketmq/rocketmq-nameserver:4.9.4-alpine-operator-0.3.0 docker push registry.zdevops.com.cn/apacherocketmq/rocketmq-broker:4.9.4-alpine-operator-0.3.0 清理临时镜像: docker rmi apache/rocketmq-operator:0.3.0 docker rmi apacherocketmq/rocketmq-nameserver:4.5.0-alpine-operator-0.3.0 docker rmi apacherocketmq/rocketmq-broker:4.5.0-alpine-operator-0.3.0 docker rmi registry.zdevops.com.cn/apacherocketmq/rocketmq-operator:0.3.0 docker rmi registry.zdevops.com.cn/apacherocketmq/rocketmq-nameserver:4.5.0-alpine-operator-0.3.0 docker rmi registry.zdevops.com.cn/apacherocketmq/rocketmq-broker:4.5.0-alpine-operator-0.3.0 制作 RocketMQ Console Image 本文直接拉取官方镜像作为本地离线镜像,如果需要修改内容并重构,可以参考 RocketMQ Console 使用的 官方 Dockerfile自行构建。 下载镜像: docker pull apacherocketmq/rocketmq-console:2.0.0 重新打 tag: docker tag apacherocketmq/rocketmq-console:2.0.0 registry.zdevops.com.cn/apacherocketmq/rocketmq-console:2.0.0 推送到私有镜像仓库: docker push registry.zdevops.com.cn/apacherocketmq/rocketmq-console:2.0.0 清理临时镜像: docker rmi apacherocketmq/rocketmq-console:2.0.0 docker rmi registry.zdevops.com.cn/apacherocketmq/rocketmq-console:2.0.0 准备单 Master RocketMQ 部署方案涉及的离线镜像 单 Master RocketMQ 部署方案涉及的镜像跟集群模式部署方案采用的 RocketMQ Operator 中使用的镜像不同,在制作离线镜像时,直接从官方镜像库拉取然后重新打 tag,再推送本地镜像仓库。 二者具体不同说明如下: 单 Master 方案使用的是 Docker Hub 中 apache 命名空间下的镜像,并且镜像名称不区分 nameserver 和 broker,RocketMQ Operator 使用的是 apacherocketmq 命名空间下的镜像,镜像名称区分 nameserver 和 broker。 单 Master 方案和 RocketMQ Operator 方案中管理工具使用的镜像也不同,单 Master 方案使用的是 apacherocketmq 命名空间下的 rocketmq-dashboard 镜像,RocketMQ Operator 使用的是 apacherocketmq 命名空间下的 rocketmq-console 镜像。 具体的离线镜像制作流程如下: 下载镜像 docker pull apache/rocketmq:4.9.4 docker pull apacherocketmq/rocketmq-dashboard:1.0.0 重新打 tag docker tag apache/rocketmq:4.9.4 registry.zdevops.com.cn/apache/rocketmq:4.9.4 docker tag apacherocketmq/rocketmq-dashboard:1.0.0 registry.zdevops.com.cn/apacherocketmq/rocketmq-dashboard:1.0.0 推送到私有镜像仓库 docker push registry.zdevops.com.cn/apache/rocketmq:4.9.4 docker push registry.zdevops.com.cn/apacherocketmq/rocketmq-dashboard:1.0.0 清理临时镜像 docker rmi apache/rocketmq:4.9.4 docker rmi apacherocketmq/rocketmq-dashboard:1.0.0 docker rmi registry.zdevops.com.cn/apache/rocketmq:4.9.4 docker rmi registry.zdevops.com.cn/apacherocketmq/rocketmq-dashboard:1.0.0 单 Master 模式部署 思路梳理 根据 RocketMQ 服务使用的组件,需要部署以下资源 Broker StatefulSet NameServer StatefulSet NameServer Cluster Service:内部服务 Dashboard Deployment Dashboard External Service:Dashboard 外部管理用 ConfigMap:Broker 自定义配置文件 资源配置清单 参考 GitHub 中 Apache rocketmq-docker项目中介绍的容器化启动示例配置,编写适用于 K8S 的资源配置清单。 Notice: 每个人技术能力、技术习惯、服务环境有所不同,这里介绍的只是我采用的一种简单方式,并不一定是最优的方案,大家可以根据实际情况编写适合自己的配置。 rocketmq-cm.yaml: kind: ConfigMap apiVersion: v1 metadata: name: rocketmq-broker-config namespace: zdevops data: BROKER_MEM: ' -Xms2g -Xmx2g -Xmn1g ' broker-common.conf: |- brokerClusterName = DefaultCluster brokerName = broker-0 brokerId = 0 deleteWhen = 04 fileReservedTime = 48 brokerRole = ASYNC_MASTER flushDiskType = ASYNC_FLUSH rocketmq-name-service-sts.yaml: kind: StatefulSet apiVersion: apps/v1 metadata: name: rocketmq-name-service namespace: zdevops spec: replicas: 1 selector: matchLabels: app: rocketmq-name-service name_service_cr: rocketmq-name-service template: metadata: labels: app: rocketmq-name-service name_service_cr: rocketmq-name-service spec: volumes: - name: host-time hostPath: path: /etc/localtime type: '' containers: - name: rocketmq-name-service image: 'registry.zdevops.com.cn/apache/rocketmq:4.9.4' command: - /bin/sh args: - mqnamesrv ports: - name: tcp-9876 containerPort: 9876 protocol: TCP resources: limits: cpu: 500m memory: 1Gi requests: cpu: 250m memory: 512Mi volumeMounts: - name: rocketmq-namesrv-storage mountPath: /home/rocketmq/logs subPath: logs - name: host-time readOnly: true mountPath: /etc/localtime imagePullPolicy: Always volumeClaimTemplates: - kind: PersistentVolumeClaim apiVersion: v1 metadata: name: rocketmq-namesrv-storage spec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi storageClassName: glusterfs volumeMode: Filesystem serviceName: '' --- kind: Service apiVersion: v1 metadata: name: rocketmq-name-server-service namespace: zdevops spec: ports: - name: tcp-9876 protocol: TCP port: 9876 targetPort: 9876 selector: name_service_cr: rocketmq-name-service type: ClusterIP rocketmq-broker-sts.yaml: kind: StatefulSet apiVersion: apps/v1 metadata: name: rocketmq-broker-0-master namespace: zdevops spec: replicas: 1 selector: matchLabels: app: rocketmq-broker broker_cr: rocketmq-broker template: metadata: labels: app: rocketmq-broker broker_cr: rocketmq-broker spec: volumes: - name: rocketmq-broker-config configMap: name: rocketmq-broker-config items: - key: broker-common.conf path: broker-common.conf defaultMode: 420 - name: host-time hostPath: path: /etc/localtime type: '' containers: - name: rocketmq-broker image: 'apache/rocketmq:4.9.4' command: - /bin/sh args: - mqbroker - "-c" - /home/rocketmq/conf/broker-common.conf ports: - name: tcp-vip-10909 containerPort: 10909 protocol: TCP - name: tcp-main-10911 containerPort: 10911 protocol: TCP - name: tcp-ha-10912 containerPort: 10912 protocol: TCP env: - name: NAMESRV_ADDR value: 'rocketmq-name-server-service.zdevops:9876' - name: BROKER_MEM valueFrom: configMapKeyRef: name: rocketmq-broker-config key: BROKER_MEM resources: limits: cpu: 500m memory: 12Gi requests: cpu: 250m memory: 2Gi volumeMounts: - name: host-time readOnly: true mountPath: /etc/localtime - name: rocketmq-broker-storage mountPath: /home/rocketmq/logs subPath: logs/broker-0-master - name: rocketmq-broker-storage mountPath: /home/rocketmq/store subPath: store/broker-0-master - name: rocketmq-broker-config mountPath: /home/rocketmq/conf/broker-common.conf subPath: broker-common.conf imagePullPolicy: Always volumeClaimTemplates: - kind: PersistentVolumeClaim apiVersion: v1 metadata: name: rocketmq-broker-storage spec: accessModes: - ReadWriteOnce resources: requests: storage: 8Gi storageClassName: glusterfs volumeMode: Filesystem serviceName: '' rocketmq-dashboard.yaml: kind: Deployment apiVersion: apps/v1 metadata: name: rocketmq-dashboard namespace: zdevops spec: replicas: 1 selector: matchLabels: app: rocketmq-dashboard template: metadata: labels: app: rocketmq-dashboard spec: containers: - name: rocketmq-dashboard image: 'registry.zdevops.com.cn/apacherocketmq/rocketmq-dashboard:1.0.0' ports: - name: http-8080 containerPort: 8080 protocol: TCP env: - name: JAVA_OPTS value: >- -Drocketmq.namesrv.addr=rocketmq-name-server-service.zdevops:9876 -Dcom.rocketmq.sendMessageWithVIPChannel=false resources: limits: cpu: 500m memory: 2Gi requests: cpu: 50m memory: 512Mi imagePullPolicy: Always --- kind: Service apiVersion: v1 metadata: name: rocketmq-dashboard-service namespace: zdevops spec: ports: - name: http-8080 protocol: TCP port: 8080 targetPort: 8080 nodePort: 31080 selector: app: rocketmq-dashboard type: NodePort GitOps 本操作为可选项,本人习惯在个人开发服务器上编辑或修改资源配置清单,然后提交到 Git 服务器 (Gitlab、Gitee、GitHub 等),然后在 k8s 节点上从 Git 服务器拉取资源配置清单并执行,从而实现资源配置清单的版本化管理,简单的实现运维 GitOps。 本系列文档的所有 k8s 资源配置清单文件,为了演示和操作方便,都放在了统一的 k8s-yaml 仓库中,实际工作中都是一个应用一个 Git 仓库,更便于应用配置的版本控制。 大家在实际使用中可以忽略本步骤,直接在 k8s 节点上编写资源配置清单并执行,也可以参考我的使用方式,实现简单的 GitOps。 在个人运维开发服务器上操作: # 在已有代码仓库创建 rocketmq/single 目录 mkdir -p rocketmq/single # 编辑资源配置清单 vi rocketmq/single/rocketmq-cm.yaml vi rocketmq/single/rocketmq-name-service-sts.yaml vi rocketmq/single/rocketmq-broker-sts.yaml vi rocketmq/single/rocketmq-dashboard.yaml # 提交 Git git add rocketmq git commit -am '添加 rocketmq 单节点资源配置清单' git push 部署资源 在 k8s 集群 Master 节点上或是独立的运维管理服务器上操作。 更新镜像仓库代码 cd /srv/k8s-yaml git pull 部署资源 (分步式,二选一) 测试环境使用分步单独部署的方式,以便测试资源配置清单的准确性。 cd /srv/k8s-yaml kubectl apply -f rocketmq/single/rocketmq-cm.yaml kubectl apply -f rocketmq/single/rocketmq-name-service-sts.yaml kubectl apply -f rocketmq/single/rocketmq-broker-sts.yaml kubectl apply -f rocketmq/single/rocketmq-dashboard.yaml 部署资源 (一键式,二选一) 实际使用中,可以直接 apply 整个目录,实现一键式自动部署,在正式研发和生产环境中使用目录的方式实现快速部署。 kubectl apply -f rocketmq/single/ 验证 ConfigMap: $ kubectl get cm -n zdevops NAME DATA AGE kube-root-ca.crt 1 17d rocketmq-broker-config 2 22s StatefulSet: $ kubectl get sts -o wide -n zdevops NAME READY AGE CONTAINERS IMAGES rocketmq-broker-0-master 1/1 11s rocketmq-broker registry.zdevops.com.cn/apache/rocketmq:4.9.4 rocketmq-name-service 1/1 12s rocketmq-name-service registry.zdevops.com.cn/apache/rocketmq:4.9.4 Deployment: $ kubectl get deploy -o wide -n zdevops NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR rocketmq-dashboard 1/1 1 1 31s rocketmq-dashboard registry.zdevops.com.cn/apacherocketmq/rocketmq-dashboard:1.0.0 app=rocketmq-dashboard Pods: $ kubectl get pods -o wide -n zdevops NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES rocketmq-broker-0-master-0 1/1 Running 0 77s 10.233.116.103 ks-k8s-master-2 <none> <none> rocketmq-dashboard-b5dbb9d88-cwhqc 1/1 Running 0 3s 10.233.87.115 ks-k8s-master-1 <none> <none> rocketmq-name-service-0 1/1 Running 0 78s 10.233.116.102 ks-k8s-master-2 <none> <none> Service: $ kubectl get svc -o wide -n zdevops NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR rocketmq-dashboard-service NodePort 10.233.5.237 <none> 8080:31080/TCP 74s app=rocketmq-dashboard rocketmq-name-server-service ClusterIP 10.233.3.61 <none> 9876/TCP 2m29s name_service_cr=rocketmq-name-service 通过浏览器打开 K8S 集群中任意节点的 IP:31080,可以看到 RocketMQ 控制台的管理界面。 清理资源 卸载 RocketMQ 或是安装失败需要清理后重新安装,可以在 K8S 集群上使用下面的流程清理资源。 清理 StatefulSet: kubectl delete sts rocketmq-broker-0-master -n zdevops kubectl delete sts rocketmq-name-service -n zdevops 清理 Deployment: kubectl delete deployments rocketmq-dashboard -n zdevops 清理 ConfigMap: kubectl delete cm rocketmq-broker-config -n zdevops 清理服务: kubectl delete svc rocketmq-name-server-service -n zdevops kubectl delete svc rocketmq-dashboard-service -n zdevops 清理存储卷: kubectl delete pvc rocketmq-namesrv-storage-rocketmq-name-service-0 -n zdevops kubectl delete pvc rocketmq-broker-storage-rocketmq-broker-0-master-0 -n zdevops 当然,也可以利用资源配置清单清理资源,更简单快捷 (存储卷无法自动清理,需要手工清理)。 $ kubectl delete -f rocketmq/single/ statefulset.apps "rocketmq-broker-0-master" deleted configmap "rocketmq-broker-config" deleted deployment.apps "rocketmq-dashboard" deleted service "rocketmq-dashboard-service" deleted statefulset.apps "rocketmq-name-service" deleted service "rocketmq-name-server-service" deleted 多 Master 多 Slave-异步复制模式部署 思路梳理 多 Master 多 Slave-异步复制模式的 RocketMQ 部署,使用官方提供的 RocketMQ Operator,部署起来比较快速便捷,扩容也比较方便。 默认配置会部署 1 个 Master 和 1 个对应的 Slave,部署完成后可以根据需求扩容 Master 和 Slave。 获取 RocketMQ Operator # git 获取代码时指定版本 cd /srv git clone -b 0.3.0 https://github.com/apache/rocketmq-operator.git 准备资源配置清单 本文演示的资源配置清单都是直接修改 rocketmq-operator 默认的配置,生产环境应根据默认配置修改一套适合自己环境的标准配置文件,并存放于 git 仓库中。 为 deploy 资源配置清单文件增加或修改命名空间: cd /srv/rocketmq-operator sed -i 'N;8 a \ namespace: zdevops' deploy/crds/rocketmq.apache.org_brokers.yaml sed -i 'N;8 a \ namespace: zdevops' deploy/crds/rocketmq.apache.org_consoles.yaml sed -i 'N;8 a \ namespace: zdevops' deploy/crds/rocketmq.apache.org_nameservices.yaml sed -i 'N;8 a \ namespace: zdevops' deploy/crds/rocketmq.apache.org_topictransfers.yaml sed -i 'N;18 a \ namespace: zdevops' deploy/operator.yaml sed -i 'N;18 a \ namespace: zdevops' deploy/role_binding.yaml sed -i 's/namespace: default/namespace: zdevops/g' deploy/role_binding.yaml sed -i 'N;18 a \ namespace: zdevops' deploy/service_account.yaml sed -i 'N;20 a \ namespace: zdevops' deploy/role.yaml 切记此步骤只能执行一次,如果失败了则需要删掉后重新执行。 执行完成后一定要查看一下结果是否符合预期 grep -r zdevops deploy/*。 修改 example 资源配置清单文件中的命名空间: sed -i 's/namespace: default/namespace: zdevops/g' example/rocketmq_v1alpha1_rocketmq_cluster.yaml sed -i 'N;18 a \ namespace: zdevops' example/rocketmq_v1alpha1_cluster_service.yaml 修改镜像地址为内网地址: sed -i 's#apache/rocketmq-operator:0.3.0#registry.zdevops.com.cn/apacherocketmq/rocketmq-operator:0.3.0#g' deploy/operator.yaml sed -i 's#apacherocketmq#registry.zdevops.com.cn/apacherocketmq#g' example/rocketmq_v1alpha1_rocketmq_cluster.yaml 修改 RocketMQ 版本 (可选): sed -i 's/4.5.0/4.9.4/g' example/rocketmq_v1alpha1_rocketmq_cluster.yaml Notice: 默认的资源配置清单示例中部署 RocketMQ 集群的版本为 4.5.0, 实际使用时请根据需求调整。 修改 NameService 网络模式 (可选): sed -i 's/hostNetwork: true/hostNetwork: false/g' example/rocketmq_v1alpha1_rocketmq_cluster.yaml sed -i 's/dnsPolicy: ClusterFirstWithHostNet/dnsPolicy: ClusterFirst/g' example/rocketmq_v1alpha1_rocketmq_cluster.yaml Notice: 官方示例默认配置使用 hostNetwork 模式 , 适用于同时给 K8S 集群内、外应用提供服务 , 实际使用时请根据需求调整 . 个人倾向于禁用 hostNetwork 模式 , 不跟外部应用混用 . 如果需要混用 , 则倾向于在外部独立部署 RocketMQ。 修改 storageClassName 为 glusterfs: sed -i 's/storageClassName: rocketmq-storage/storageClassName: glusterfs/g' example/rocketmq_v1alpha1_rocketmq_cluster.yaml sed -i 's/storageMode: EmptyDir/storageMode: StorageClass/g' example/rocketmq_v1alpha1_rocketmq_cluster.yaml Notice: 演示环境 GlusterFS 存储对应的 storageClassName 为 glusterfs,请根据实际情况修改。 修改 nameServers 为域名的形式: sed -i 's/nameServers: ""/nameServers: "name-server-service.zdevops:9876"/g' example/rocketmq_v1alpha1_rocketmq_cluster.yaml Notice: name-server-service.zdevops 是 NameServer service 名称 + 项目名称的组合 默认配置采用 pod [ip:port] 的形式 , 一旦 Pod IP 发生变化 ,Console 就没法管理集群了 , 且 Console 不会自动变更配置,如果设置为空的话可能还会出现随便配置的情况,因此一定要提前修改。 修改 RocketMQ Console 外部访问的 NodePort: sed -i 's/nodePort: 30000/nodePort: 31080/g' example/rocketmq_v1alpha1_cluster_service.yaml Notice: 官方示例默认配置端口号为 30000, 实际使用时请根据需求调整。 修改 RocketMQ NameServer 和 Console 的 service 配置: sed -i '32,46s/^#//g' example/rocketmq_v1alpha1_cluster_service.yaml sed -i 's/nodePort: 30001/nodePort: 31081/g' example/rocketmq_v1alpha1_cluster_service.yaml sed -i 's/namespace: default/namespace: zdevops/g' example/rocketmq_v1alpha1_cluster_service.yaml NameServer 默认使用了 NodePort 的形式,单纯在 K8S 集群内部使用的话,可以修改为集群模式。 GitOps 生产环境实际使用时建议将上面编辑整理后的资源配置清单,单独整理,删除 rocketmq-operator 项目中多余的文件,行成一套适合于自己业务需要的资源配置清单,并使用 Git 进行版本控制。 单 Master 模式部署方案中已经详细介绍过操作流程,此处不再多做介绍。 4.5. 部署 RocketMQ Operator (自动) 官方介绍的自动部署方法,适用于能连接互联网的环境,部署过程中需要下载 controller-gen 和 kustomize 二进制文件,同时会下载一堆 go 依赖。 不适合于内网离线环境,这里只是简单介绍,本文重点采用后面的手动部署的方案。 部署 RocketMQ Operator: make deploy 部署 RocketMQ Operator (手动) 部署 RocketMQ Operator: kubectl create -f deploy/crds/rocketmq.apache.org_brokers.yaml kubectl create -f deploy/crds/rocketmq.apache.org_nameservices.yaml kubectl create -f deploy/crds/rocketmq.apache.org_consoles.yaml kubectl create -f deploy/crds/rocketmq.apache.org_topictransfers.yaml kubectl create -f deploy/service_account.yaml kubectl create -f deploy/role.yaml kubectl create -f deploy/role_binding.yaml kubectl create -f deploy/operator.yaml 验证 CRDS: $ kubectl get crd | grep rocketmq.apache.org brokers.rocketmq.apache.org 2022-11-09T02:54:52Z consoles.rocketmq.apache.org 2022-11-09T02:54:54Z nameservices.rocketmq.apache.org 2022-11-09T02:54:53Z topictransfers.rocketmq.apache.org 2022-11-09T02:54:54Z 验证 RocketMQ Operator: $ kubectl get deploy -n zdevops -o wide NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR rocketmq-operator 1/1 1 1 6m46s manager registry.zdevops.com.cn/apacherocketmq/rocketmq-operator:0.3.0 name=rocketmq-operator $ kubectl get pods -n zdevops -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES rocketmq-operator-7cc6b48796-htpk8 1/1 Running 0 2m28s 10.233.116.70 ks-k8s-master-2 <none> <none> 部署 RocketMQ 集群 创建服务: $ kubectl apply -f example/rocketmq_v1alpha1_cluster_service.yaml service/console-service created service/name-server-service created 创建集群: $ kubectl apply -f example/rocketmq_v1alpha1_rocketmq_cluster.yaml configmap/broker-config created broker.rocketmq.apache.org/broker created nameservice.rocketmq.apache.org/name-service created console.rocketmq.apache.org/console created 验证 StatefulSet: $ kubectl get sts -o wide -n zdevops NAME READY AGE CONTAINERS IMAGES broker-0-master 1/1 27s broker registry.zdevops.com.cn/apacherocketmq/rocketmq-broker:4.9.4-alpine-operator-0.3.0 broker-0-replica-1 1/1 27s broker registry.zdevops.com.cn/apacherocketmq/rocketmq-broker:4.9.4-alpine-operator-0.3.0 name-service 1/1 27s name-service registry.zdevops.com.cn/apacherocketmq/rocketmq-nameserver:4.9.4-alpine-operator-0.3.0 Deployment: $ kubectl get deploy -o wide -n zdevops NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR console 1/1 1 1 52s console registry.zdevops.com.cn/apacherocketmq/rocketmq-console:2.0.0 app=rocketmq-console rocketmq-operator 1/1 1 1 4h43m manager registry.zdevops.com.cn/apacherocketmq/rocketmq-operator:0.3.0 name=rocketmq-operator Pod: $ kubectl get pods -o wide -n zdevops NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES broker-0-master-0 1/1 Running 0 47s 10.233.87.24 ks-k8s-master-1 <none> <none> broker-0-replica-1-0 1/1 Running 0 17s 10.233.117.28 ks-k8s-master-0 <none> <none> console-8d685798f-5pwct 1/1 Running 0 116s 10.233.116.84 ks-k8s-master-2 <none> <none> name-service-0 1/1 Running 0 96s 10.233.116.85 ks-k8s-master-2 <none> <none> rocketmq-operator-7cc6b48796-htpk8 1/1 Running 2 (98s ago) 4h39m 10.233.116.70 ks-k8s-master-2 <none> <none> Services: $ kubectl get svc -o wide -n zdevops NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR console-service NodePort 10.233.38.15 <none> 8080:31080/TCP 21m app=rocketmq-console name-server-service NodePort 10.233.56.238 <none> 9876:31081/TCP 21m name_service_cr=name-service 通过浏览器打开 K8S 集群中任意节点的 IP:31080,可以看到 RocketMQ 控制台的管理界面。 清理资源 清理 RocketMQ Cluster 部署集群失败或是需要重新部署时,采用下面的顺序清理删除。 kubectl delete -f example/rocketmq_v1alpha1_rocketmq_cluster.yaml kubectl delete -f example/rocketmq_v1alpha1_cluster_service.yaml 清理 RocketMQ Operator kubectl delete -f deploy/crds/rocketmq.apache.org_brokers.yaml kubectl delete -f deploy/crds/rocketmq.apache.org_nameservices.yaml kubectl delete -f deploy/crds/rocketmq.apache.org_consoles.yaml kubectl delete -f deploy/crds/rocketmq.apache.org_topictransfers.yaml kubectl delete -f deploy/service_account.yaml kubectl delete -f deploy/role.yaml kubectl delete -f deploy/role_binding.yaml kubectl delete -f deploy/operator.yaml 清理存储卷 需要手工查找 Broker 和 NameServer 相关的存储卷并删除。 # 查找存储卷 $ kubectl get pvc -n zdevops NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE broker-storage-broker-0-master-0 Bound pvc-6a78b573-d72a-47ca-9012-5bc888dfcb0f 8Gi RWO glusterfs 3m54s broker-storage-broker-0-replica-1-0 Bound pvc-4f096942-505d-4e34-ac7f-b871b9f33df3 8Gi RWO glusterfs 3m54s namesrv-storage-name-service-0 Bound pvc-2c45a77e-3ca1-4eab-bb57-8374aa9068d3 1Gi RWO glusterfs 3m54s # 删除存储卷 kubectl delete pvc namesrv-storage-name-service-0 -n zdevops kubectl delete pvc broker-storage-broker-0-master-0 -n zdevops kubectl delete pvc broker-storage-broker-0-replica-1-0 -n zdevops 扩容 NameServer 如果当前的 name service 集群规模不能满足您的需求,您可以简单地使用 RocketMQ-Operator 来扩大或缩小 name service 集群的规模。 扩容 name service 需要编写并执行独立的资源配置清单,参考官方示例Name Server Cluster Scale,并结合自己实际环境的 rocketmq-operator 配置修改。 Notice: 不要在已部署的资源中直接修改副本数,直接修改不会生效,会被 Operator 干掉。 编辑扩容 NameServer 资源配置清单 , rocketmq_v1alpha1_nameservice_cr.yaml: apiVersion: rocketmq.apache.org/v1alpha1 kind: NameService metadata: name: name-service namespace: zdevops spec: size: 2 nameServiceImage: registry.zdevops.com.cn/apacherocketmq/rocketmq-nameserver:4.9.4-alpine-operator-0.3.0 imagePullPolicy: Always hostNetwork: false dnsPolicy: ClusterFirst resources: requests: memory: "512Mi" cpu: "250m" limits: memory: "1024Mi" cpu: "500m" storageMode: StorageClass hostPath: /data/rocketmq/nameserver volumeClaimTemplates: - metadata: name: namesrv-storage spec: accessModes: - ReadWriteOnce storageClassName: glusterfs resources: requests: storage: 1Gi 执行扩容操作: kubectl apply -f rocketmq/cluster/rocketmq_v1alpha1_nameservice_cr.yaml 验证 StatefulSet: $ kubectl get sts name-service -o wide -n zdevops NAME READY AGE CONTAINERS IMAGES name-service 2/2 16m name-service registry.zdevops.com.cn/apacherocketmq/rocketmq-nameserver:4.9.4-alpine-operator-0.3.0 验证 Pods: $ kubectl get pods -o wide -n zdevops NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES broker-0-master-0 1/1 Running 0 18m 10.233.87.117 ks-k8s-master-1 <none> <none> broker-0-replica-1-0 1/1 Running 0 43s 10.233.117.99 ks-k8s-master-0 <none> <none> console-8d685798f-hnmvg 1/1 Running 0 18m 10.233.116.113 ks-k8s-master-2 <none> <none> name-service-0 1/1 Running 0 18m 10.233.116.114 ks-k8s-master-2 <none> <none> name-service-1 1/1 Running 0 110s 10.233.87.120 ks-k8s-master-1 <none> <none> rocketmq-operator-6db8ccc685-5hkk8 1/1 Running 0 18m 10.233.116.112 ks-k8s-master-2 <none> <none> 特别说明 NameServer 扩容一定要慎重,在实际验证测试中发现 NameServer 扩容会导致重建已有的除了 Broker-0 的 Master 之外的其他 Broker 的 Master 和 所有的 Slave。按官方文档上的说明,应该是 Operator 通知所有的 Broker 更新 name service list parameters,以便它们可以注册到新的 NameServer Service。 同时,在 allowRestart: true 策略下,Broker 将逐渐更新,因此更新过程也不会被生产者和消费者客户端感知,也就是说理论上不会影响业务(未实际测试)。 但是,所有 Broker 的 Master 和 Slave 重建后,查看集群状态时,集群节点的信息不稳定,有的时候能看到 3 个节点,有的时候则能看到 4 个节点。 因此,生产环境最好在初次部署的时候就配置 NameServer 的副本数为 2 或是 3,尽量不要在后期扩容,除非你能搞定扩容造成的一切后果。 扩容 Broker 通常情况下,随着业务的发展,现有的 Broker 集群规模可能不再满足您的业务需求。你可以简单地使用 RocketMQ-Operator 来升级、扩容 Broker 集群。 扩容 Broker 需要编写并执行独立的资源配置清单,参考官方示例Broker Cluster Scale,并结合自己实际环境的 rocketmq-operator 配置修改。 编辑扩容 Broker 资源配置清单 , rocketmq_v1alpha1_broker_cr.yaml: apiVersion: rocketmq.apache.org/v1alpha1 kind: Broker metadata: name: broker namespace: zdevops spec: size: 2 nameServers: "name-server-service.zdevops::9876" replicaPerGroup: 1 brokerImage: registry.zdevops.com.cn/apacherocketmq/rocketmq-broker:4.9.4-alpine-operator-0.3.0 imagePullPolicy: Always resources: requests: memory: "2048Mi" cpu: "250m" limits: memory: "12288Mi" cpu: "500m" allowRestart: true storageMode: StorageClass hostPath: /data/rocketmq/broker # scalePodName is [Broker name]-[broker group number]-master-0 scalePodName: broker-0-master-0 env: - name: BROKER_MEM valueFrom: configMapKeyRef: name: broker-config key: BROKER_MEM volumes: - name: broker-config configMap: name: broker-config items: - key: broker-common.conf path: broker-common.conf volumeClaimTemplates: - metadata: name: broker-storage spec: accessModes: - ReadWriteOnce storageClassName: glusterfs resources: requests: storage: 8Gi Notice: 注意重点字段 scalePodName: broker-0-master-0。 选择源 Broker pod,将从其中将主题和订阅信息数据等旧元数据传输到新创建的 Broker。 执行扩容 Broker: kubectl apply -f rocketmq/cluster/rocketmq_v1alpha1_broker_cr.yaml Notice: 执行成功后将部署一个新的 Broker Pod 组,同时 Operator 将在启动新 Broker 之前将源 Broker Pod 中的元数据复制到新创建的 Broker Pod 中,因此新 Broker 将重新加载已有的主题和订阅信息。 验证 StatefulSet: $ kubectl get sts -o wide -n zdevops NAME READY AGE CONTAINERS IMAGES broker-0-master 1/1 43m broker registry.zdevops.com.cn/apacherocketmq/rocketmq-broker:4.9.4-alpine-operator-0.3.0 broker-0-replica-1 1/1 43m broker registry.zdevops.com.cn/apacherocketmq/rocketmq-broker:4.9.4-alpine-operator-0.3.0 broker-1-master 1/1 27s broker registry.zdevops.com.cn/apacherocketmq/rocketmq-broker:4.9.4-alpine-operator-0.3.0 broker-1-replica-1 1/1 27s broker registry.zdevops.com.cn/apacherocketmq/rocketmq-broker:4.9.4-alpine-operator-0.3.0 name-service 2/2 43m name-service registry.zdevops.com.cn/apacherocketmq/rocketmq-nameserver:4.9.4-alpine-operator-0.3.0 验证 Pods: $ kubectl get pods -o wide -n zdevops NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES broker-0-master-0 1/1 Running 0 44m 10.233.87.117 ks-k8s-master-1 <none> <none> broker-0-replica-1-0 1/1 Running 0 26m 10.233.117.99 ks-k8s-master-0 <none> <none> broker-1-master-0 1/1 Running 0 72s 10.233.116.117 ks-k8s-master-2 <none> <none> broker-1-replica-1-0 1/1 Running 0 72s 10.233.117.100 ks-k8s-master-0 <none> <none> console-8d685798f-hnmvg 1/1 Running 0 44m 10.233.116.113 ks-k8s-master-2 <none> <none> name-service-0 1/1 Running 0 44m 10.233.116.114 ks-k8s-master-2 <none> <none> name-service-1 1/1 Running 0 27m 10.233.87.120 ks-k8s-master-1 <none> <none> rocketmq-operator-6db8ccc685-5hkk8 1/1 Running 0 44m 10.233.116.112 ks-k8s-master-2 <none> <none> 在 KubeSphere 控制台验证: 在 RocketMQ 控制台验证: 常见问题 没装 gcc 编译工具 报错信息: [root@zdevops-master rocketmq-operator]# make docker-build IMG=${IMAGE_URL} /data/k8s-yaml/rocketmq-operator/bin/controller-gen rbac:roleName=rocketmq-operator crd:generateEmbeddedObjectMeta=true webhook paths="./..." output:dir=deploy output:crd:artifacts:config=deploy/crds head -n 14 deploy/role_binding.yaml > deploy/role.yaml.bak cat deploy/role.yaml >> deploy/role.yaml.bak rm deploy/role.yaml && mv deploy/role.yaml.bak deploy/role.yaml /data/k8s-yaml/rocketmq-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..." /usr/local/go/src/net/cgo_linux.go:12:8: no such package located Error: not all generators ran successfully run `controller-gen object:headerFile=hack/boilerplate.go.txt paths=./... -w` to see all available markers, or `controller-gen object:headerFile=hack/boilerplate.go.txt paths=./... -h` for usage make: *** [generate] Error 1 解决方案: $ yum install gcc go mod 错误 报错信息: # 执行 make docker-build IMG=${IMAGE_URL} 报错 go: creating new go.mod: module tmp Downloading sigs.k8s.io/controller-tools/cmd/controller-gen@v0.7.0 go get: added sigs.k8s.io/controller-tools v0.7.0 /data/build/rocketmq-operator/bin/controller-gen rbac:roleName=rocketmq-operator crd:generateEmbeddedObjectMeta=true webhook paths="./..." output:dir=deploy output:crd:artifacts:config=deploy/crds Error: err: exit status 1: stderr: go: github.com/google/uuid@v1.1.2: missing go.sum entry; to add it: go mod download github.com/google/uuid Usage: controller-gen [flags] ...... output rules (optionally as output:<generator>:...) +output:artifacts[:code=<string>],config=<string> package outputs artifacts to different locations, depending on whether they're package-associated or not. +output:dir=<string> package outputs each artifact to the given directory, regardless of if it's package-associated or not. +output:none package skips outputting anything. +output:stdout package outputs everything to standard-out, with no separation. run `controller-gen rbac:roleName=rocketmq-operator crd:generateEmbeddedObjectMeta=true webhook paths=./... output:dir=deploy output:crd:artifacts:config=deploy/crds -w` to see all available markers, or `controller-gen rbac:roleName=rocketmq-operator crd:generateEmbeddedObjectMeta=true webhook paths=./... output:dir=deploy output:crd:artifacts:config=deploy/crds -h` for usage make: *** [manifests] Error 1 解决方案: go mod tidy 结束语 本文只是初步介绍了 RocketMQ 在 K8s 平台上的单 Master 节点和多 Master 多 Slave-异步复制模式部署的方法,属于入门级。 在生产环境中还需要根据实际环境优化配置,例如调整集群的 Broker 数量、Master 和 Slave 的分配、性能调优、配置优化等。 本文由博客一文多发平台 OpenWrite 发布!

优秀的个人博客,低调大师

SPI:Java的高可扩展利器

摘要:JAVA SPI,基于接口的编程+策略模式+配置文件的动态加载机制。 本文分享自华为云社区《一文讲透Java核心技术之高可扩展利器SPI》,作者: 冰 河。 SPI的概念 JAVA SPI = 基于接口的编程+策略模式+配置文件 的动态加载机制 SPI的使用场景 Java是一种面向对象语言,虽然Java8开始支持函数式编程和Stream,但是总体来说,还是面向对象的语言。在使用Java进行面向对象开发时,一般会推荐使用基于接口的编程,程序的模块与模块之前不会直接进行实现类的硬编码。而在实际的开发过程中,往往一个接口会有多个实现类,各实现类要么实现的逻辑不同,要么使用的方式不同,还有的就是实现的技术不同。为了使调用方在调用接口的时候,明确的知道自己调用的是接口的哪个实现类,或者说为了实现在模块装配的时候不用在程序里动态指明,这就需要一种服务发现机制。Java中的SPI加载机制能够满足这样的需求,它能够自动寻找某个接口的实现类。 大量的框架使用了Java的SPI技术,如下: (1)JDBC加载不同类型的数据库驱动 (2)日志门面接口实现类加载,SLF4J加载不同提供商的日志实现类 (3)Spring中大量使用了SPI 对servlet3.0规范 对ServletContainerInitializer的实现 自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等 (4)Dubbo里面有很多个组件,每个组件在框架中都是以接口的形成抽象出来!具体的实现又分很多种,在程序执行时根据用户的配置来按需取接口的实现 SPI的使用 当服务的提供者,提供了接口的一种实现后,需要在Jar包的**META-INF/services/**目录下,创建一个以接口的名称(包名.接口名的形式)命名的文件,在文件中配置接口的实现类(完整的包名+类名)。 当外部程序通过java.util.ServiceLoader类装载这个接口时,就能够通过该Jar包的**META/Services/**目录里的配置文件找到具体的实现类名,装载实例化,完成注入。同时,SPI的规范规定了接口的实现类必须有一个无参构造方法。 SPI中查找接口的实现类是通过java.util.ServiceLoader,而在java.util.ServiceLoader类中有一行代码如下: // 加载具体实现类信息的前缀,也就是以接口命名的文件需要放到Jar包中的META-INF/services/目录下 private static final String PREFIX = "META-INF/services/"; 这也就是说,我们必须将接口的配置文件写到Jar包的**META/Services/**目录下。 SPI实例 这里,给出一个简单的SPI使用实例,演示在Java程序中如何使用SPI动态加载接口的实现类。 注意:实例是基于Java8进行开发的。 1.创建Maven项目 在IDEA中创建Maven项目spi-demo,如下: 2.编辑pom.xml <?xml version="1.0" encoding="UTF-8"?> <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"> <artifactId>spi-demo</artifactId> <groupId>io.binghe.spi</groupId> <packaging>jar</packaging> <version>1.0.0-SNAPSHOT</version> <modelVersion>4.0.0</modelVersion> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.6.0</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build> </project> 3.创建类加载工具类 在io.binghe.spi.loader包下创建MyServiceLoader,MyServiceLoader类中直接调用JDK的ServiceLoader类加载Class。代码如下所示。 package io.binghe.spi.loader; import java.util.ServiceLoader; /** * @author binghe * @version 1.0.0 * @description 类加载工具 */ public class MyServiceLoader { /** * 使用SPI机制加载所有的Class */ public static <S> ServiceLoader<S> loadAll(final Class<S> clazz) { return ServiceLoader.load(clazz); } } 4.创建接口 在io.binghe.spi.service包下创建接口MyService,作为测试接口,接口中只有一个方法,打印传入的字符串信息。代码如下所示: package io.binghe.spi.service; /** * @author binghe * @version 1.0.0 * @description 定义接口 */ public interface MyService { /** * 打印信息 */ void print(String info); } 5.创建接口的实现类 (1)创建第一个实现类MyServiceA 在io.binghe.spi.service.impl包下创建MyServiceA类,实现MyService接口。代码如下所示: package io.binghe.spi.service.impl; import io.binghe.spi.service.MyService; /** * @author binghe * @version 1.0.0 * @description 接口的第一个实现 */ public class MyServiceA implements MyService { @Override public void print(String info) { System.out.println(MyServiceA.class.getName() + " print " + info); } } (2)创建第二个实现类MyServiceB 在io.binghe.spi.service.impl包下创建MyServiceB类,实现MyService接口。代码如下所示: package io.binghe.spi.service.impl; import io.binghe.spi.service.MyService; /** * @author binghe * @version 1.0.0 * @description 接口第二个实现 */ public class MyServiceB implements MyService { @Override public void print(String info) { System.out.println(MyServiceB.class.getName() + " print " + info); } } 6.创建接口文件 在项目的src/main/resources目录下创建**META/Services/**目录,在目录中创建io.binghe.spi.service.MyService文件,注意:文件必须是接口MyService的全名,之后将实现MyService接口的类配置到文件中,如下所示: io.binghe.spi.service.impl.MyServiceA io.binghe.spi.service.impl.MyServiceB 7.创建测试类 在项目的io.binghe.spi.main包下创建Main类,该类为测试程序的入口类,提供一个main()方法,在main()方法中调用ServiceLoader类加载MyService接口的实现类。并通过Java8的Stream将结果打印出来,如下所示: package io.binghe.spi.main; import io.binghe.spi.loader.MyServiceLoader; import io.binghe.spi.service.MyService; import java.util.ServiceLoader; import java.util.stream.StreamSupport; /** * @author binghe * @version 1.0.0 * @description 测试的main方法 */ public class Main { public static void main(String[] args){ ServiceLoader<MyService> loader = MyServiceLoader.loadAll(MyService.class); StreamSupport.stream(loader.spliterator(), false).forEach(s -> s.print("Hello World")); } } 8.测试实例 运行Main类中的main()方法,打印出的信息如下所示: io.binghe.spi.service.impl.MyServiceA print Hello World io.binghe.spi.service.impl.MyServiceB print Hello World Process finished with exit code 0 通过打印信息可以看出,通过Java SPI机制正确加载出接口的实现类,并调用接口的实现方法。 源码解析 这里,主要是对SPI的加载流程涉及到的java.util.ServiceLoader的源码的解析。 进入java.util.ServiceLoader的源码,可以看到ServiceLoader类实现了java.lang.Iterable接口,如下所示。 public final class ServiceLoader<S> implements Iterable<S> 说明ServiceLoader类是可以遍历迭代的。 java.util.ServiceLoader类中定义了如下的成员变量: // 加载具体实现类信息的前缀,也就是以接口命名的文件需要放到Jar包中的META-INF/services/目录下 private static final String PREFIX = "META-INF/services/"; // 需要加载的接口 private final Class<S> service; // 类加载器,用于加载以接口命名的文件中配置的接口的实现类 private final ClassLoader loader; // 创建ServiceLoader时采用的访问控制上下文环境 private final AccessControlContext acc; // 用来缓存已经加载的接口实现类,其中,Key是接口实现类的完整类名,Value为实现类对象 private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); // 用于延迟加载实现类的迭代器 private LazyIterator lookupIterator; 可以看到ServiceLoader类中定义了加载前缀为“META-INF/services/”,所以,接口文件必须要在项目的src/main/resources目录下的**META-INF/services/**目录下创建。 从MyServiceLoader类调用**ServiceLoader.load(clazz)**方法进入源码,如下所示: //根据类的Class对象加载指定的类,返回ServiceLoader对象 public static <S> ServiceLoader<S> load(Class<S> service) { //获取当前线程的类加载器 ClassLoader cl = Thread.currentThread().getContextClassLoader(); //动态加载指定的类,将类加载到ServiceLoader中 return ServiceLoader.load(service, cl); } 方法中调用了**ServiceLoader.load(service, cl)**方法,继续跟踪代码,如下所示: //通过ClassLoader加载指定类的Class,并将返回结果封装到ServiceLoader对象中 public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){ return new ServiceLoader<>(service, loader); } 可以看到**ServiceLoader.load(service, cl)**方法中,调用了ServiceLoader类的构造方法,继续跟进代码,如下所示: //构造ServiceLoader对象 private ServiceLoader(Class<S> svc, ClassLoader cl) { //如果传入的Class对象为空,则判处空指针异常 service = Objects.requireNonNull(svc, "Service interface cannot be null"); //如果传入的ClassLoader为空,则通过ClassLoader.getSystemClassLoader()获取,否则直接使用传入的ClassLoader loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl; acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null; reload(); } 继续跟**reload()**方法,如下所示。 //重新加载 public void reload() { //清空保存加载的实现类的LinkedHashMap providers.clear(); //构造延迟加载的迭代器 lookupIterator = new LazyIterator(service, loader); } 继续跟进懒加载迭代器的构造函数,如下所示。 private LazyIterator(Class<S> service, ClassLoader loader) { this.service = service; this.loader = loader; } 可以看到,会将需要加载的接口的Class对象和类加载器赋值给LazyIterator的成员变量。 当我们在程序中迭代获取对象实例时,首先在成员变量**providers中查找是否有缓存的实例对象。如果存在则直接返回,否则调用lookupIterator**延迟加载迭代器进行加载。 迭代器进行逻辑判断的代码如下所示: //迭代ServiceLoader的方法 public Iterator<S> iterator() { return new Iterator<S>() { //获取保存实现类的LinkedHashMap<String,S>的迭代器 Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator(); //判断是否有下一个元素 public boolean hasNext() { //如果knownProviders存在元素,则直接返回true if (knownProviders.hasNext()) return true; //返回延迟加载器是否存在元素 return lookupIterator.hasNext(); } //获取下一个元素 public S next() { //如果knownProviders存在元素,则直接获取 if (knownProviders.hasNext()) return knownProviders.next().getValue(); //获取延迟迭代器lookupIterator中的元素 return lookupIterator.next(); } public void remove() { throw new UnsupportedOperationException(); } }; } LazyIterator加载类的流程如下代码所示 //判断是否拥有下一个实例 private boolean hasNextService() { //如果拥有下一个实例,直接返回true if (nextName != null) { return true; } //如果实现类的全名为null if (configs == null) { try { //获取全文件名,文件相对路径+文件名称(包名+接口名) String fullName = PREFIX + service.getName(); //类加载器为空,则通过ClassLoader.getSystemResources()方法获取 if (loader == null) configs = ClassLoader.getSystemResources(fullName); else //类加载器不为空,则直接通过类加载器获取 configs = loader.getResources(fullName); } catch (IOException x) { fail(service, "Error locating configuration files", x); } } while ((pending == null) || !pending.hasNext()) { //如果configs中没有更过的元素,则直接返回false if (!configs.hasMoreElements()) { return false; } //解析包结构 pending = parse(service, configs.nextElement()); } nextName = pending.next(); return true; } private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); String cn = nextName; nextName = null; Class<?> c = null; try { //加载类对象 c = Class.forName(cn, false, loader); } catch (ClassNotFoundException x) { fail(service, "Provider " + cn + " not found"); } if (!service.isAssignableFrom(c)) { fail(service, "Provider " + cn + " not a subtype"); } try { //通过c.newInstance()生成对象实例 S p = service.cast(c.newInstance()); //将生成的对象实例保存到缓存中(LinkedHashMap<String,S>) providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider " + cn + " could not be instantiated", x); } throw new Error(); // This cannot happen } public boolean hasNext() { if (acc == null) { return hasNextService(); } else { PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() { public Boolean run() { return hasNextService(); } }; return AccessController.doPrivileged(action, acc); } } public S next() { if (acc == null) { return nextService(); } else { PrivilegedAction<S> action = new PrivilegedAction<S>() { public S run() { return nextService(); } }; return AccessController.doPrivileged(action, acc); } } 最后,给出整个java.util.ServiceLoader的类,如下所示: package java.util; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.security.AccessControlContext; import java.security.AccessController; import java.security.PrivilegedAction; public final class ServiceLoader<S> implements Iterable<S> { // 加载具体实现类信息的前缀,也就是以接口命名的文件需要放到Jar包中的META-INF/services/目录下 private static final String PREFIX = "META-INF/services/"; // 需要加载的接口 private final Class<S> service; // 类加载器,用于加载以接口命名的文件中配置的接口的实现类 private final ClassLoader loader; // 创建ServiceLoader时采用的访问控制上下文环境 private final AccessControlContext acc; // 用来缓存已经加载的接口实现类,其中,Key是接口实现类的完整类名,Value为实现类对象 private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); // 用于延迟加载实现类的迭代器 private LazyIterator lookupIterator; //重新加载 public void reload() { //清空保存加载的实现类的LinkedHashMap providers.clear(); //构造延迟加载的迭代器 lookupIterator = new LazyIterator(service, loader); } //构造ServiceLoader对象 private ServiceLoader(Class<S> svc, ClassLoader cl) { //如果传入的Class对象为空,则判处空指针异常 service = Objects.requireNonNull(svc, "Service interface cannot be null"); //如果传入的ClassLoader为空,则通过ClassLoader.getSystemClassLoader()获取,否则直接使用传入的ClassLoader loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl; acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null; reload(); } private static void fail(Class<?> service, String msg, Throwable cause) throws ServiceConfigurationError { throw new ServiceConfigurationError(service.getName() + ": " + msg, cause); } private static void fail(Class<?> service, String msg) throws ServiceConfigurationError { throw new ServiceConfigurationError(service.getName() + ": " + msg); } private static void fail(Class<?> service, URL u, int line, String msg) throws ServiceConfigurationError { fail(service, u + ":" + line + ": " + msg); } // Parse a single line from the given configuration file, adding the name // on the line to the names list. // private int parseLine(Class<?> service, URL u, BufferedReader r, int lc, List<String> names) throws IOException, ServiceConfigurationError { String ln = r.readLine(); if (ln == null) { return -1; } int ci = ln.indexOf('#'); if (ci >= 0) ln = ln.substring(0, ci); ln = ln.trim(); int n = ln.length(); if (n != 0) { if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0)) fail(service, u, lc, "Illegal configuration-file syntax"); int cp = ln.codePointAt(0); if (!Character.isJavaIdentifierStart(cp)) fail(service, u, lc, "Illegal provider-class name: " + ln); for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) { cp = ln.codePointAt(i); if (!Character.isJavaIdentifierPart(cp) && (cp != '.')) fail(service, u, lc, "Illegal provider-class name: " + ln); } if (!providers.containsKey(ln) && !names.contains(ln)) names.add(ln); } return lc + 1; } private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError { InputStream in = null; BufferedReader r = null; ArrayList<String> names = new ArrayList<>(); try { in = u.openStream(); r = new BufferedReader(new InputStreamReader(in, "utf-8")); int lc = 1; while ((lc = parseLine(service, u, r, lc, names)) >= 0); } catch (IOException x) { fail(service, "Error reading configuration file", x); } finally { try { if (r != null) r.close(); if (in != null) in.close(); } catch (IOException y) { fail(service, "Error closing configuration file", y); } } return names.iterator(); } // Private inner class implementing fully-lazy provider lookupload private class LazyIterator implements Iterator<S> { Class<S> service; ClassLoader loader; Enumeration<URL> configs = null; Iterator<String> pending = null; String nextName = null; private LazyIterator(Class<S> service, ClassLoader loader) { this.service = service; this.loader = loader; } //判断是否拥有下一个实例 private boolean hasNextService() { //如果拥有下一个实例,直接返回true if (nextName != null) { return true; } //如果实现类的全名为null if (configs == null) { try { //获取全文件名,文件相对路径+文件名称(包名+接口名) String fullName = PREFIX + service.getName(); //类加载器为空,则通过ClassLoader.getSystemResources()方法获取 if (loader == null) configs = ClassLoader.getSystemResources(fullName); else //类加载器不为空,则直接通过类加载器获取 configs = loader.getResources(fullName); } catch (IOException x) { fail(service, "Error locating configuration files", x); } } while ((pending == null) || !pending.hasNext()) { //如果configs中没有更过的元素,则直接返回false if (!configs.hasMoreElements()) { return false; } //解析包结构 pending = parse(service, configs.nextElement()); } nextName = pending.next(); return true; } private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); String cn = nextName; nextName = null; Class<?> c = null; try { //加载类对象 c = Class.forName(cn, false, loader); } catch (ClassNotFoundException x) { fail(service, "Provider " + cn + " not found"); } if (!service.isAssignableFrom(c)) { fail(service, "Provider " + cn + " not a subtype"); } try { //通过c.newInstance()生成对象实例 S p = service.cast(c.newInstance()); //将生成的对象实例保存到缓存中(LinkedHashMap<String,S>) providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider " + cn + " could not be instantiated", x); } throw new Error(); // This cannot happen } public boolean hasNext() { if (acc == null) { return hasNextService(); } else { PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() { public Boolean run() { return hasNextService(); } }; return AccessController.doPrivileged(action, acc); } } public S next() { if (acc == null) { return nextService(); } else { PrivilegedAction<S> action = new PrivilegedAction<S>() { public S run() { return nextService(); } }; return AccessController.doPrivileged(action, acc); } } public void remove() { throw new UnsupportedOperationException(); } } //迭代ServiceLoader的方法 public Iterator<S> iterator() { return new Iterator<S>() { //获取保存实现类的LinkedHashMap<String,S>的迭代器 Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator(); //判断是否有下一个元素 public boolean hasNext() { //如果knownProviders存在元素,则直接返回true if (knownProviders.hasNext()) return true; //返回延迟加载器是否存在元素 return lookupIterator.hasNext(); } //获取下一个元素 public S next() { //如果knownProviders存在元素,则直接获取 if (knownProviders.hasNext()) return knownProviders.next().getValue(); //获取延迟迭代器lookupIterator中的元素 return lookupIterator.next(); } public void remove() { throw new UnsupportedOperationException(); } }; } //通过ClassLoader加载指定类的Class,并将返回结果封装到ServiceLoader对象中 public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) { return new ServiceLoader<>(service, loader); } //根据类的Class对象加载指定的类,返回ServiceLoader对象 public static <S> ServiceLoader<S> load(Class<S> service) { //获取当前线程的类加载器 ClassLoader cl = Thread.currentThread().getContextClassLoader(); //动态加载指定的类,将类加载到ServiceLoader中 return ServiceLoader.load(service, cl); } public static <S> ServiceLoader<S> loadInstalled(Class<S> service) { ClassLoader cl = ClassLoader.getSystemClassLoader(); ClassLoader prev = null; while (cl != null) { prev = cl; cl = cl.getParent(); } return ServiceLoader.load(service, prev); } /** * Returns a string describing this service. * * @return A descriptive string */ public String toString() { return "java.util.ServiceLoader[" + service.getName() + "]"; } } SPI总结 优点: 能够实现项目解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。 缺点: 多个并发多线程使用ServiceLoader类的实例是不安全的 虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。 参考:深入理解Java中的spi机制 点击关注,第一时间了解华为云新鲜技术~

优秀的个人博客,低调大师

搭建MHA实现MySQL集群高可用

MHA:Master High Availability,对主节点进行监控,可实现自动故障转移至其它从节点;通过提升某一从节点为新的主节点,基于主从复制实现,还需要客户端配合实现,目前MHA主要支持一主多从的架构,要搭建MHA,要求一个复制集群中必须最少有三台数据库服务器,一主二从,即一台充当master,一台充当备用master,另外一台充当从库。 MHA工作原理: MHA利用 SELECT 1 As Value 指令判断master服务器的健康性,一旦master 宕机,MHA 从宕机崩溃的master保存二进制日志事件(binlog events) 识别含有最新更新的slave 应用差异的中继日志(relay log)到其他的slave 应用从master保存的二进制日志事件(binlog events) 提升一个slave为新的master 使其他的slave连接新的master进行复制 MHA软件 MHA软件由两部分组成,Manager工具包和Node工具包 Manager工具包主要包括以下几个工具: masterha_check_sshmasterha_check_replmasterha_mangermasterha_check_statusmasterha_master_monitormasterha_master_switchmasterha_conf_hostmasterha_stop--conf=app1.cnfmasterha_secondary_check #检查MHA的SSH配置状况#检查MySQL复制状况#启动MHA#检测当前MHA运行状态#检测master是否宕机#故障转移(自动或手动)#添加或删除配置的server信息#停止MHA#两个或多个网络线路检查MySQL主服务器的可用 Node工具包: 这些工具通常由MHA Manager的脚本触发,无需人为操作)主要包括以下几个工具: save_binary_logs apply_diff_relay_logs filter_mysqlbinlog purge_relay_logs #保存和复制master的二进制日志#识别差异的中继日志事件并将其差异的事件应用于其他的slave#去除不必要的ROLLBACK事件(MHA已不再使用此工具)#清除中继日志(不会阻塞SQL线程) MHA自定义扩展: secondary_check_script master_ip_ailover_script shutdown_script report_script init_conf_load_scriptmaster_ip_online_change_script #通过多条网络路由检测master的可用性#更新Application使用的masterip#强制关闭master节点#发送报告#加载初始配置参数#更新master节点ip地址 MHA配置文件: global配置,为各application提供默认配置,默认文件路径 /etc/masterha_default.cnfapplication配置:为每个主从复制集群 实现MHA实战案例 环境:四台主机 10.0.0.7 CentOS7 MHA管理端10.0.0.8 CentOS8 MySQL8.0 Master10.0.0.18CentOS8 MySQL8.0 Slave110.0.0.28 CentOS8 MySQL8.0 Slave2 1.在管理节点上安装两个包mha4mysql-manager和mha4mysql-node 说明: mha4mysql-manager-0.56-0.el6.noarch.rpm 不支持CentOS 8,只支持CentOS7 以下版本mha4mysql-manager-0.58-0.el7.centos.noarch.rpm 支持MySQL5.7和MySQL8.0 ,但和CentOS8版本上的Mariadb -10.3.17不兼容 两个安装包: mha4mysql-managermha4mysql-node 管理端安装两个RPM包: [root@MHA-Manager~]#ll total144 -rw-------.1rootroot1764Apr2412:19anaconda-ks.cfg -rw-r--r--1rootroot0Apr2811:20a.txt -rw-r--r--1rootroot587May1221:13CentOS-8.repo -rw-r--r--1rootroot920Apr2914:50f1.txt -rw-r--r--1rootroot1601Apr2914:50f2.txt -rw-r--r--1rootroot1076May209:38ks-centos8.cfg -rw-r--r--1rootroot949Apr2814:35ks.cfg -rw-r--r--1rootroot81024Jul302020mha4mysql-manager-0.58-0.el7.centos.noarch.rpm -rw-r--r--1rootroot36328Jul302020mha4mysql-node-0.58-0.el7.centos.noarch.rpm -rw-r--r--1rootroot41Apr2708:50test.sh drwxr-xr-x.2rootroot220Apr2414:46yum #这里一定要先装mha4mysql-node,然后再装mha4mysql-manager [root@MHA-Manager~]#yuminstall-ymha4mysql-node-0.58-0.el7.centos.noarch.rpm [root@MHA-Manager~]#yuminstall-ymha4mysql-manager-0.58-0.el7.centos.noarch.rpm 2.在所有MySQL服务器上安装mha4mysql-node包 [root@Master~]#yuminstall-ymha4mysql-node-0.58-0.el7.centos.noarch.rpm [root@Slave1~]#yuminstall-ymha4mysql-node-0.58-0.el7.centos.noarch.rpm [root@Slave2~]#yuminstall-ymha4mysql-node-0.58-0.el7.centos.noarch.rpm 3.在所有节点实现相互之间ssh key验证 [root@MHA-Manager~]#ssh-keygen Generatingpublic/privatersakeypair. Enterfileinwhichtosavethekey(/root/.ssh/id_rsa): Createddirectory'/root/.ssh'. Enterpassphrase(emptyfornopassphrase): Entersamepassphraseagain: Youridentificationhasbeensavedin/root/.ssh/id_rsa. Yourpublickeyhasbeensavedin/root/.ssh/id_rsa.pub. Thekeyfingerprintis: SHA256:C9ovIIe3BWcMBFTZgs0pi9ECVq7166py11oDCLgWfNMroot@MHA-Manager.magedu.com Thekey'srandomartimageis: +---[RSA2048]----+ |o++Oo+| |*=O.| |o*BE| |.o*.++| |.o...=.S| |.o++o..| |+.*+.| |..+.oo| |.o.ooo..| +----[SHA256]-----+ [root@MHA-Manager~]#ssh-copy-id127.0.0.1 [root@MHA-Manager~]#rsync-av.ssh10.0.0.8:/root/ [root@MHA-Manager~]#rsync-av.ssh10.0.0.18:/root/ [root@MHA-Manager~]#rsync-av.ssh10.0.0.28:/root/ 4.在管理节点建立配置文件 [root@MHA-Manager~]#mkdir/etc/mastermha/ [root@MHA-Manager~]#vim/etc/mastermha/app1.conf [serverdefault] user=mhauser#用于远程连接MySQL所有节点的用户,需要有管理员的权限 password=magedu manager_workdir=/data/mastermha/app1/#目录会自动生成,无需手动创建 manager_log=/data/mastermha/app1/manager.log remote_workdir=/data/mastermha/app1/ ssh_user=root#用于实现远程ssh基于KEY的连接,访问二进制日志 repl_user=repluser#主从复制的用户信息 repl_password=magedu ping_interval=1#健康性检查的时间间隔 master_ip_failover_script=/usr/local/bin/master_ip_failover#切换VIP的perl脚本 report_script=/usr/local/bin/sendmail.sh#当执行报警脚本 check_repl_delay=0#默认值为1,表示如果slave中从库落后主库relaylog超过100M,主库不会选择这个从库为新的master,因为这个从库进行恢复需要很长的时间.通过设置参数check_repl_delay=0,mha触发主从切换时会忽略复制的延时,对于设置candidate_master=1的从库非常有用,这样确保这个从库一定能成为最新的master master_binlog_dir=/data/mysql/#指定二进制日志存放的目录,mha4mysql-manager-0.58必须指定,之前版本不需要指定 [server1] hostname=10.0.0.8 candidate_master=1 [server2] hostname=10.0.0.18 candidate_master=1#设置为优先候选master,即使不是集群中事件最新的slave,也会优先当master [server3] hostname=10.0.0.28 #最终文件内容 [root@MHA-Manager~]#cat/etc/mastermha/app1.conf [serverdefault] user=mhauser password=magedu manager_workdir=/data/mastermha/app1/ manager_log=/data/mastermha/app1/manager.log remote_workdir=/data/mastermha/app1/ ssh_user=root repl_user=repluser repl_password=magedu ping_interval=1 master_ip_failover_script=/usr/local/bin/master_ip_failover report_script=/usr/local/bin/sendmail.sh check_repl_delay=0 master_binlog_dir=/data/mysql/ [server1] hostname=10.0.0.8 candidate_master=1 [server2] hostname=10.0.0.18 candidate_master=1 [server3] hostname=10.0.0.28 [root@MHA-Manager~]# 说明:主库宕机谁来接管新的Master 1. 所有从节点日志都是一致的,默认会以配置文件的顺序去选择一个新主2. 从节点日志不一致,自动选择最接近于主库的从库充当新主3. 如果对于某节点设定了权重(candidate_master=1),权重节点会优先选择。但是此节点日志量落后主库超过100M日志的话,也不会被选择。可以配合check_repl_delay=0,关闭日志量的检查,强制选择候选节点 5.相关脚本 [root@MHA-Manager~]#cat/usr/local/bin/sendmail.sh #!/bin/bash echo"MySQLisdown"|mail-s"MHAWarning"15762354477@139.com [root@MHA-Manager~]#chmod+x/usr/local/bin/sendmail.sh [root@MHA-Manager~]#vim/usr/local/bin/master_ip_failover #!/usr/bin/envperl usestrict; usewarningsFATAL=>'all'; useGetopt::Long; my( $command,$ssh_user,$orig_master_host,$orig_master_ip, $orig_master_port,$new_master_host,$new_master_ip,$new_master_port ); #执行时必须删除下面三行注释 my$vip='10.0.0.100/24';#设置VirtualIP my$gateway='10.0.0.254';#网关GatewayIP my$interface='eth0';#指定VIP所在网卡 my$key="1"; my$ssh_start_vip="/sbin/ifconfig$interface:$key$vip;/sbin/arping-I$interface-c3-s$vip$gateway>/dev/null2>&1"; my$ssh_stop_vip="/sbin/ifconfig$interface:$keydown"; GetOptions( 'command=s'=>\$command, 'ssh_user=s'=>\$ssh_user, 'orig_master_host=s'=>\$orig_master_host, 'orig_master_ip=s'=>\$orig_master_ip, 'orig_master_port=i'=>\$orig_master_port, 'new_master_host=s'=>\$new_master_host, 'new_master_ip=s'=>\$new_master_ip, 'new_master_port=i'=>\$new_master_port, ); exit&main(); submain{ print"\n\nINSCRIPTTEST====$ssh_stop_vip==$ssh_start_vip===\n\n"; if($commandeq"stop"||$commandeq"stopssh"){ #$orig_master_host,$orig_master_ip,$orig_master_portarepassed. #Ifyoumanagemasteripaddressatglobalcatalogdatabase, #invalidateorig_master_iphere. my$exit_code=1; eval{ print"DisablingtheVIPonoldmaster:$orig_master_host\n"; &stop_vip(); $exit_code=0; }; if($@){ warn"GotError:$@\n"; exit$exit_code; } exit$exit_code; } elsif($commandeq"start"){ #allargumentsarepassed. ##Ifyoumanagemasteripaddressatglobalcatalogdatabase, ##activatenew_master_iphere. ##Youcanalsograntwriteaccess(createuser,setread_only=0,etc)here. my$exit_code=10; eval{ print"EnablingtheVIP-$viponthenewmaster-$new_master_host\n"; &start_vip(); $exit_code=0; }; if($@){ warn$@; exit$exit_code; } exit$exit_code; } elsif($commandeq"status"){ print"CheckingtheStatusofthescript..OK\n"; `ssh$ssh_user\@$orig_master_host\"$ssh_start_vip\"`; exit0; } else{ &usage(); exit1; } } #AsimplesystemcallthatenabletheVIPonthenewmaster substart_vip(){ `ssh$ssh_user\@$new_master_host\"$ssh_start_vip\"`; } #AsimplesystemcallthatdisabletheVIPontheold_master substop_vip(){ `ssh$ssh_user\@$orig_master_host\"$ssh_stop_vip\"`; } subusage{ print "Usage:master_ip_failover--command=start|stop|stopssh|status-- orig_master_host=host--orig_master_ip=ip--orig_master_port=port-- new_master_host=host--new_master_ip=ip--new_master_port=port\n"; } #最终文件内容 [root@MHA-Manager~]#cat/usr/local/bin/master_ip_failover #!/usr/bin/envperl usestrict; usewarningsFATAL=>'all'; useGetopt::Long; my( $command,$ssh_user,$orig_master_host,$orig_master_ip, $orig_master_port,$new_master_host,$new_master_ip,$new_master_port ); my$vip='10.0.0.100/24'; my$gateway='10.0.0.254'; my$interface='eth0'; my$key="1"; my$ssh_start_vip="/sbin/ifconfig$interface:$key$vip;/sbin/arping-I$interface-c3-s$vip$gateway>/dev/null2>&1"; my$ssh_stop_vip="/sbin/ifconfig$interface:$keydown"; GetOptions( 'command=s'=>\$command, 'ssh_user=s'=>\$ssh_user, 'orig_master_host=s'=>\$orig_master_host, 'orig_master_ip=s'=>\$orig_master_ip, 'orig_master_port=i'=>\$orig_master_port, 'new_master_host=s'=>\$new_master_host, 'new_master_ip=s'=>\$new_master_ip, 'new_master_port=i'=>\$new_master_port, ); exit&main(); submain{ print"\n\nINSCRIPTTEST====$ssh_stop_vip==$ssh_start_vip===\n\n"; if($commandeq"stop"||$commandeq"stopssh"){ #$orig_master_host,$orig_master_ip,$orig_master_portarepassed. #Ifyoumanagemasteripaddressatglobalcatalogdatabase, #invalidateorig_master_iphere. my$exit_code=1; eval{ print"DisablingtheVIPonoldmaster:$orig_master_host\n"; &stop_vip(); $exit_code=0; }; if($@){ warn"GotError:$@\n"; exit$exit_code; } exit$exit_code; } elsif($commandeq"start"){ #allargumentsarepassed. ##Ifyoumanagemasteripaddressatglobalcatalogdatabase, ##activatenew_master_iphere. ##Youcanalsograntwriteaccess(createuser,setread_only=0,etc)here. my$exit_code=10; eval{ print"EnablingtheVIP-$viponthenewmaster-$new_master_host\n"; &start_vip(); $exit_code=0; }; if($@){ warn$@; exit$exit_code; } exit$exit_code; } elsif($commandeq"status"){ print"CheckingtheStatusofthescript..OK\n"; `ssh$ssh_user\@$orig_master_host\"$ssh_start_vip\"`; exit0; } else{ &usage(); exit1; } } #AsimplesystemcallthatenabletheVIPonthenewmaster substart_vip(){ `ssh$ssh_user\@$new_master_host\"$ssh_start_vip\"`; } #AsimplesystemcallthatdisabletheVIPontheold_master substop_vip(){ `ssh$ssh_user\@$orig_master_host\"$ssh_stop_vip\"`; } subusage{ print "Usage:master_ip_failover--command=start|stop|stopssh|status-- orig_master_host=host--orig_master_ip=ip--orig_master_port=port-- new_master_host=host--new_master_ip=ip--new_master_port=port\n"; } [root@MHA-Manager~]#chmod+x/usr/local/bin/master_ip_failover [root@MHA-Manager~]# 6.实现Master [root@Master~]#yuminstall-ymysql-server [root@Master~]#mkdir/data/mysql/ [root@Master~]#chownmysql:mysql/data/mysql/ [root@Master~]#vim/etc/my.cnf.d/mysql-server.cnf [mysqld] datadir=/var/lib/mysql socket=/var/lib/mysql/mysql.sock log-error=/var/log/mysql/mysqld.log pid-file=/run/mysqld/mysqld.pid server-id=8 log-bin=/data/mysql/mysql-bin skip_name_resolve=1 general_log#观察结果,非必须项,生产无需启用 [root@Master~]#systemctlenable--nowmysqld.service [root@Master~]#mysql WelcometotheMySQLmonitor.Commandsendwith;or\g. YourMySQLconnectionidis8 Serverversion:8.0.21Sourcedistribution Copyright(c)2000,2020,Oracleand/oritsaffiliates.Allrightsreserved. OracleisaregisteredtrademarkofOracleCorporationand/orits affiliates.Othernamesmaybetrademarksoftheirrespective owners. Type'help;'or'\h'forhelp.Type'\c'toclearthecurrentinputstatement. mysql>showmasterlogs; +------------------+-----------+-----------+ |Log_name|File_size|Encrypted| +------------------+-----------+-----------+ |mysql-bin.000001|179|No| |mysql-bin.000002|1201|No| +------------------+-----------+-----------+ 2rowsinset(0.00sec) mysql>createuserrepluser@'10.0.0.%'identifiedby'magedu'; QueryOK,0rowsaffected(0.01sec) mysql>grantreplicationslaveon*.*torepluser@'10.0.0.%'; QueryOK,0rowsaffected(0.00sec) mysql>createusermhauser@'10.0.0.%'identifiedby'magedu'; QueryOK,0rowsaffected(0.01sec) mysql>grantallon*.*tomhauser@'10.0.0.%'; QueryOK,0rowsaffected(0.01sec) mysql>selectuser,hostfrommysql.user; +------------------+-----------+ |user|host| +------------------+-----------+ |mhauser|10.0.0.%| |repluser|10.0.0.%| |mysql.infoschema|localhost| |mysql.session|localhost| |mysql.sys|localhost| |root|localhost| +------------------+-----------+ 6rowsinset(0.00sec) mysql>quit Bye #配置VIP [root@Master~]#ifconfigeth0:110.0.0.100/24 [root@Master~]#ifconfig eth0:flags=4163<UP,BROADCAST,RUNNING,MULTICAST>mtu1500 inet10.0.0.8netmask255.255.255.0broadcast10.0.0.255 inet6fe80::809c:7c3f:dc61:53bbprefixlen64scopeid0x20<link> ether00:0c:29:0a:08:a3txqueuelen1000(Ethernet) RXpackets67605bytes91484149(87.2MiB) RXerrors0dropped0overruns0frame0 TXpackets31053bytes2925668(2.7MiB) TXerrors0dropped0overruns0carrier0collisions0 eth0:1:flags=4163<UP,BROADCAST,RUNNING,MULTICAST>mtu1500 inet10.0.0.100netmask255.255.255.0broadcast10.0.0.255 ether00:0c:29:0a:08:a3txqueuelen1000(Ethernet) lo:flags=73<UP,LOOPBACK,RUNNING>mtu65536 inet127.0.0.1netmask255.0.0.0 inet6::1prefixlen128scopeid0x10<host> looptxqueuelen1000(LocalLoopback) RXpackets0bytes0(0.0B) RXerrors0dropped0overruns0frame0 TXpackets0bytes0(0.0B) TXerrors0dropped0overruns0carrier0collisions0 [root@Master~]# 7.实现Slave [root@Slave1~]#yuminstall-ymysql-server [root@Slave1~]#mkdir/data/mysql/ [root@Slave1~]#chownmysql:mysql/data/mysql/ [root@Slave1~]#vim/etc/my.cnf.d/mysql-server.cnf [mysqld] datadir=/var/lib/mysql socket=/var/lib/mysql/mysql.sock log-error=/var/log/mysql/mysqld.log pid-file=/run/mysqld/mysqld.pid server_id=18#不同节点此值各不相同 log-bin=/data/mysql/mysql-bin read_only relay_log_purge=0 skip_name_resolve=1#禁止反向解析 general_log#方便观察的设置,生产无需启用 [root@Slave1~]#systemctlenable--nowmysqld.service [root@Slave1~]#mysql WelcometotheMySQLmonitor.Commandsendwith;or\g. YourMySQLconnectionidis8 Serverversion:8.0.21Sourcedistribution Copyright(c)2000,2020,Oracleand/oritsaffiliates.Allrightsreserved. OracleisaregisteredtrademarkofOracleCorporationand/orits affiliates.Othernamesmaybetrademarksoftheirrespective owners. Type'help;'or'\h'forhelp.Type'\c'toclearthecurrentinputstatement. mysql>CHANGEMASTERTO ->MASTER_HOST='10.0.0.100', ->MASTER_USER='repluser', ->MASTER_PASSWORD='magedu', ->MASTER_LOG_FILE='mysql-bin.000002', ->MASTER_LOG_POS=1201; QueryOK,0rowsaffected,2warnings(0.05sec) mysql>startslave; QueryOK,0rowsaffected(0.01sec) mysql>showslavestatus\G ***************************1.row*************************** Slave_IO_State:Waitingformastertosendevent Master_Host:10.0.0.100 Master_User:repluser Master_Port:3306 Connect_Retry:60 Master_Log_File:mysql-bin.000002 Read_Master_Log_Pos:1201 Relay_Log_File:Slave1-relay-bin.000002 Relay_Log_Pos:324 Relay_Master_Log_File:mysql-bin.000002 Slave_IO_Running:Yes Slave_SQL_Running:Yes Replicate_Do_DB: Replicate_Ignore_DB: Replicate_Do_Table: Replicate_Ignore_Table: Replicate_Wild_Do_Table: Replicate_Wild_Ignore_Table: Last_Errno:0 Last_Error: Skip_Counter:0 Exec_Master_Log_Pos:1201 Relay_Log_Space:534 Until_Condition:None Until_Log_File: Until_Log_Pos:0 Master_SSL_Allowed:No Master_SSL_CA_File: Master_SSL_CA_Path: Master_SSL_Cert: Master_SSL_Cipher: Master_SSL_Key: Seconds_Behind_Master:0 Master_SSL_Verify_Server_Cert:No Last_IO_Errno:0 Last_IO_Error: Last_SQL_Errno:0 Last_SQL_Error: Replicate_Ignore_Server_Ids: Master_Server_Id:8 Master_UUID:bb4f4671-baa2-11eb-8cf4-000c290a08a3 Master_Info_File:mysql.slave_master_info SQL_Delay:0 SQL_Remaining_Delay:NULL Slave_SQL_Running_State:Slavehasreadallrelaylog;waitingformoreupdates Master_Retry_Count:86400 Master_Bind: Last_IO_Error_Timestamp: Last_SQL_Error_Timestamp: Master_SSL_Crl: Master_SSL_Crlpath: Retrieved_Gtid_Set: Executed_Gtid_Set: Auto_Position:0 Replicate_Rewrite_DB: Channel_Name: Master_TLS_Version: Master_public_key_path: Get_master_public_key:0 Network_Namespace: 1rowinset(0.00sec) mysql>quit Bye [root@Slave1~]# [root@Slave2~]#yuminstall-ymysql-server [root@Slave2~]#mkdir-p/data/mysql/ [root@Slave2~]#chownmysql:mysql/data/mysql/ [root@Slave2~]#vim/etc/my.cnf.d/mysql-server.cnf [mysqld] datadir=/var/lib/mysql socket=/var/lib/mysql/mysql.sock log-error=/var/log/mysql/mysqld.log pid-file=/run/mysqld/mysqld.pid server_id=28#不同节点此值各不相同 log-bin=/data/mysql/mysql-bin read_only relay_log_purge=0 skip_name_resolve=1#禁止反向解析 general_log#方便观察的设置,生产无需启用 [root@Slave2~]#systemctlenable--nowmysqld.service [root@Slave2~]#mysql WelcometotheMySQLmonitor.Commandsendwith;or\g. YourMySQLconnectionidis8 Serverversion:8.0.21Sourcedistribution Copyright(c)2000,2020,Oracleand/oritsaffiliates.Allrightsreserved. OracleisaregisteredtrademarkofOracleCorporationand/orits affiliates.Othernamesmaybetrademarksoftheirrespective owners. Type'help;'or'\h'forhelp.Type'\c'toclearthecurrentinputstatement. mysql>CHANGEMASTERTO ->MASTER_HOST='10.0.0.100', ->MASTER_USER='repluser', ->MASTER_PASSWORD='magedu', ->MASTER_LOG_FILE='mysql-bin.000002', ->MASTER_LOG_POS=1201; QueryOK,0rowsaffected,2warnings(0.03sec) mysql>startslave; QueryOK,0rowsaffected(0.00sec) mysql>showslavestatus\G ***************************1.row*************************** Slave_IO_State:Waitingformastertosendevent Master_Host:10.0.0.100 Master_User:repluser Master_Port:3306 Connect_Retry:60 Master_Log_File:mysql-bin.000002 Read_Master_Log_Pos:1201 Relay_Log_File:Slave2-relay-bin.000002 Relay_Log_Pos:324 Relay_Master_Log_File:mysql-bin.000002 Slave_IO_Running:Yes Slave_SQL_Running:Yes Replicate_Do_DB: Replicate_Ignore_DB: Replicate_Do_Table: Replicate_Ignore_Table: Replicate_Wild_Do_Table: Replicate_Wild_Ignore_Table: Last_Errno:0 Last_Error: Skip_Counter:0 Exec_Master_Log_Pos:1201 Relay_Log_Space:534 Until_Condition:None Until_Log_File: Until_Log_Pos:0 Master_SSL_Allowed:No Master_SSL_CA_File: Master_SSL_CA_Path: Master_SSL_Cert: Master_SSL_Cipher: Master_SSL_Key: Seconds_Behind_Master:0 Master_SSL_Verify_Server_Cert:No Last_IO_Errno:0 Last_IO_Error: Last_SQL_Errno:0 Last_SQL_Error: Replicate_Ignore_Server_Ids: Master_Server_Id:8 Master_UUID:bb4f4671-baa2-11eb-8cf4-000c290a08a3 Master_Info_File:mysql.slave_master_info SQL_Delay:0 SQL_Remaining_Delay:NULL Slave_SQL_Running_State:Slavehasreadallrelaylog;waitingformoreupdates Master_Retry_Count:86400 Master_Bind: Last_IO_Error_Timestamp: Last_SQL_Error_Timestamp: Master_SSL_Crl: Master_SSL_Crlpath: Retrieved_Gtid_Set: Executed_Gtid_Set: Auto_Position:0 Replicate_Rewrite_DB: Channel_Name: Master_TLS_Version: Master_public_key_path: Get_master_public_key:0 Network_Namespace: 1rowinset(0.00sec) mysql>quit Bye [root@Slave2~]# 8.检查MHA的环境 [root@MHA-Manager~]#masterha_check_ssh--conf=/etc/mastermha/app1.conf SatMay2210:40:362021-[warning]Globalconfigurationfile/etc/masterha_default.cnfnotfound.Skipping. SatMay2210:40:362021-[info]Readingapplicationdefaultconfigurationfrom/etc/mastermha/app1.conf.. SatMay2210:40:362021-[info]Readingserverconfigurationfrom/etc/mastermha/app1.conf.. SatMay2210:40:362021-[info]StartingSSHconnectiontests.. SatMay2210:40:372021-[debug] SatMay2210:40:362021-[debug]ConnectingviaSSHfromroot@10.0.0.8(10.0.0.8:22)toroot@10.0.0.18(10.0.0.18:22).. SatMay2210:40:362021-[debug]ok. SatMay2210:40:362021-[debug]ConnectingviaSSHfromroot@10.0.0.8(10.0.0.8:22)toroot@10.0.0.28(10.0.0.28:22).. Warning:Permanentlyadded'10.0.0.28'(ECDSA)tothelistofknownhosts. SatMay2210:40:372021-[debug]ok. SatMay2210:40:372021-[debug] SatMay2210:40:362021-[debug]ConnectingviaSSHfromroot@10.0.0.18(10.0.0.18:22)toroot@10.0.0.8(10.0.0.8:22).. SatMay2210:40:372021-[debug]ok. SatMay2210:40:372021-[debug]ConnectingviaSSHfromroot@10.0.0.18(10.0.0.18:22)toroot@10.0.0.28(10.0.0.28:22).. SatMay2210:40:372021-[debug]ok. SatMay2210:40:382021-[debug] SatMay2210:40:372021-[debug]ConnectingviaSSHfromroot@10.0.0.28(10.0.0.28:22)toroot@10.0.0.8(10.0.0.8:22).. SatMay2210:40:372021-[debug]ok. SatMay2210:40:372021-[debug]ConnectingviaSSHfromroot@10.0.0.28(10.0.0.28:22)toroot@10.0.0.18(10.0.0.18:22).. SatMay2210:40:382021-[debug]ok. SatMay2210:40:382021-[info]AllSSHconnectiontestspassedsuccessfully. [root@MHA-Manager~]#masterha_check_repl--conf=/etc/mastermha/app1.conf SatMay2210:43:062021-[warning]Globalconfigurationfile/etc/masterha_default.cnfnotfound.Skipping. SatMay2210:43:062021-[info]Readingapplicationdefaultconfigurationfrom/etc/mastermha/app1.conf.. SatMay2210:43:062021-[info]Readingserverconfigurationfrom/etc/mastermha/app1.conf.. SatMay2210:43:062021-[info]MHA::MasterMonitorversion0.58. Creatingdirectory/data/mastermha/app1/..done. SatMay2210:43:062021-[error][/usr/share/perl5/vendor_perl/MHA/Server.pm,ln180]GotMySQLerrorwhenconnecting10.0.0.18(10.0.0.18:3306):1130:Host'10.0.0.7'isnotallowedtoconnecttothisMySQLserver,butthisisnotaMySQLcrash.CheckMySQLserversettings. SatMay2210:43:062021-[error][/usr/share/perl5/vendor_perl/MHA/Server.pm,ln180]GotMySQLerrorwhenconnecting10.0.0.28(10.0.0.28:3306):1130:Host'10.0.0.7'isnotallowedtoconnecttothisMySQLserver,butthisisnotaMySQLcrash.CheckMySQLserversettings. SatMay2210:43:062021-[error][/usr/share/perl5/vendor_perl/MHA/ServerManager.pm,ln301]at/usr/share/perl5/vendor_perl/MHA/ServerManager.pmline297. SatMay2210:43:062021-[error][/usr/share/perl5/vendor_perl/MHA/ServerManager.pm,ln301]at/usr/share/perl5/vendor_perl/MHA/ServerManager.pmline297. SatMay2210:43:072021-[error][/usr/share/perl5/vendor_perl/MHA/ServerManager.pm,ln309]Gotfatalerror,stoppingoperations SatMay2210:43:072021-[error][/usr/share/perl5/vendor_perl/MHA/MasterMonitor.pm,ln427]Errorhappenedoncheckingconfigurations.at/usr/share/perl5/vendor_perl/MHA/MasterMonitor.pmline329. SatMay2210:43:072021-[error][/usr/share/perl5/vendor_perl/MHA/MasterMonitor.pm,ln525]Errorhappenedonmonitoringservers. SatMay2210:43:072021-[info]Gotexitcode1(Notmasterdead). MySQLReplicationHealthisNOTOK! #这里Slave从节点的健康性检查失败是因为Slave从节点的数据库中不存在repluser和mhauser用户,造成管理节点连不到两个从节点服务器,那么这里我在两个从节点上创建这两个账户用户并进行授权 [root@Slave1~]#mysql WelcometotheMySQLmonitor.Commandsendwith;or\g. YourMySQLconnectionidis12 Serverversion:8.0.21Sourcedistribution Copyright(c)2000,2020,Oracleand/oritsaffiliates.Allrightsreserved. OracleisaregisteredtrademarkofOracleCorporationand/orits affiliates.Othernamesmaybetrademarksoftheirrespective owners. Type'help;'or'\h'forhelp.Type'\c'toclearthecurrentinputstatement. mysql>createuserrepluser@'10.0.0.%'identifiedby'magedu'; QueryOK,0rowsaffected(0.01sec) mysql>grantreplicationslaveon*.*torepluser@'10.0.0.%'; QueryOK,0rowsaffected(0.00sec) mysql>createusermhauser@'10.0.0.%'identifiedby'magedu'; QueryOK,0rowsaffected(0.01sec) mysql>grantallon*.*tomhauser@'10.0.0.%'; QueryOK,0rowsaffected(0.00sec) mysql>selectuser,hostfrommysql.user; +------------------+-----------+ |user|host| +------------------+-----------+ |mhauser|10.0.0.%| |repluser|10.0.0.%| |mysql.infoschema|localhost| |mysql.session|localhost| |mysql.sys|localhost| |root|localhost| +------------------+-----------+ 6rowsinset(0.00sec) mysql>quit Bye [root@Slave2~]#mysql WelcometotheMySQLmonitor.Commandsendwith;or\g. YourMySQLconnectionidis12 Serverversion:8.0.21Sourcedistribution Copyright(c)2000,2020,Oracleand/oritsaffiliates.Allrightsreserved. OracleisaregisteredtrademarkofOracleCorporationand/orits affiliates.Othernamesmaybetrademarksoftheirrespective owners. Type'help;'or'\h'forhelp.Type'\c'toclearthecurrentinputstatement. mysql>createuserrepluser@'10.0.0.%'identifiedby'magedu'; QueryOK,0rowsaffected(0.01sec) mysql>grantreplicationslaveon*.*torepluser@'10.0.0.%'; QueryOK,0rowsaffected(0.00sec) mysql>createusermhauser@'10.0.0.%'identifiedby'magedu'; QueryOK,0rowsaffected(0.01sec) mysql>grantallon*.*tomhauser@'10.0.0.%'; QueryOK,0rowsaffected(0.00sec) mysql>selectuser,hostfrommysql.user; +------------------+-----------+ |user|host| +------------------+-----------+ |mhauser|10.0.0.%| |repluser|10.0.0.%| |mysql.infoschema|localhost| |mysql.session|localhost| |mysql.sys|localhost| |root|localhost| +------------------+-----------+ 6rowsinset(0.00sec) mysql>quit Bye #重新检查环境,检查正常 [root@MHA-Manager~]#masterha_check_repl--conf=/etc/mastermha/app1.conf SatMay2215:42:572021-[warning]Globalconfigurationfile/etc/masterha_default.cnfnotfound.Skipping. SatMay2215:42:572021-[info]Readingapplicationdefaultconfigurationfrom/etc/mastermha/app1.conf.. SatMay2215:42:572021-[info]Readingserverconfigurationfrom/etc/mastermha/app1.conf.. SatMay2215:42:572021-[info]MHA::MasterMonitorversion0.58. SatMay2215:42:582021-[info]GTIDfailovermode=0 SatMay2215:42:582021-[info]DeadServers: SatMay2215:42:582021-[info]AliveServers: SatMay2215:42:582021-[info]10.0.0.8(10.0.0.8:3306) SatMay2215:42:582021-[info]10.0.0.18(10.0.0.18:3306) SatMay2215:42:582021-[info]10.0.0.28(10.0.0.28:3306) SatMay2215:42:582021-[info]AliveSlaves: SatMay2215:42:582021-[info]10.0.0.18(10.0.0.18:3306)Version=8.0.21(oldestmajorversionbetweenslaves)log-bin:enabled SatMay2215:42:582021-[info]Replicatingfrom10.0.0.8(10.0.0.8:3306) SatMay2215:42:582021-[info]PrimarycandidateforthenewMaster(candidate_masterisset) SatMay2215:42:582021-[info]10.0.0.28(10.0.0.28:3306)Version=8.0.21(oldestmajorversionbetweenslaves)log-bin:enabled SatMay2215:42:582021-[info]Replicatingfrom10.0.0.8(10.0.0.8:3306) SatMay2215:42:582021-[info]CurrentAliveMaster:10.0.0.8(10.0.0.8:3306) SatMay2215:42:582021-[info]Checkingslaveconfigurations.. SatMay2215:42:582021-[info]Checkingreplicationfilteringsettings.. SatMay2215:42:582021-[info]binlog_do_db=,binlog_ignore_db= SatMay2215:42:582021-[info]Replicationfilteringcheckok. SatMay2215:42:582021-[info]GTID(withauto-pos)isnotsupported SatMay2215:42:582021-[info]StartingSSHconnectiontests.. SatMay2215:43:002021-[info]AllSSHconnectiontestspassedsuccessfully. SatMay2215:43:002021-[info]CheckingMHANodeversion.. SatMay2215:43:012021-[info]Versioncheckok. SatMay2215:43:012021-[info]CheckingSSHpublickeyauthenticationsettingsonthecurrentmaster.. SatMay2215:43:012021-[info]HealthCheck:SSHto10.0.0.8isreachable. SatMay2215:43:012021-[info]MasterMHANodeversionis0.58. SatMay2215:43:012021-[info]Checkingrecoveryscriptconfigurationson10.0.0.8(10.0.0.8:3306).. SatMay2215:43:012021-[info]Executingcommand:save_binary_logs--command=test--start_pos=4--binlog_dir=/data/mysql/--output_file=/data/mastermha/app1//save_binary_logs_test--manager_version=0.58--start_file=mysql-bin.000007 SatMay2215:43:012021-[info]Connectingtoroot@10.0.0.8(10.0.0.8:22).. Creating/data/mastermha/app1ifnotexists..ok. Checkingoutputdirectoryisaccessibleornot.. ok. Binlogfoundat/data/mysql/,uptomysql-bin.000007 SatMay2215:43:012021-[info]Binlogsettingcheckdone. SatMay2215:43:012021-[info]CheckingSSHpublickeyauthenticationandcheckingrecoveryscriptconfigurationsonallaliveslaveservers.. SatMay2215:43:012021-[info]Executingcommand:apply_diff_relay_logs--command=test--slave_user='mhauser'--slave_host=10.0.0.18--slave_ip=10.0.0.18--slave_port=3306--workdir=/data/mastermha/app1/--target_version=8.0.21--manager_version=0.58--relay_dir=/var/lib/mysql--current_relay_log=Slave1-relay-bin.000008--slave_pass=xxx SatMay2215:43:012021-[info]Connectingtoroot@10.0.0.18(10.0.0.18:22).. Checkingslaverecoveryenvironmentsettings.. Relaylogfoundat/var/lib/mysql,uptoSlave1-relay-bin.000008 Temporaryrelaylogfileis/var/lib/mysql/Slave1-relay-bin.000008 Checkingifsuper_read_onlyisdefinedandturnedon..notpresentorturnedoff,ignoring. Testingmysqlconnectionandprivileges.. mysql:[Warning]Usingapasswordonthecommandlineinterfacecanbeinsecure. done. Testingmysqlbinlogoutput..done. Cleaninguptestfile(s)..done. SatMay2215:43:022021-[info]Executingcommand:apply_diff_relay_logs--command=test--slave_user='mhauser'--slave_host=10.0.0.28--slave_ip=10.0.0.28--slave_port=3306--workdir=/data/mastermha/app1/--target_version=8.0.21--manager_version=0.58--relay_dir=/var/lib/mysql--current_relay_log=Slave2-relay-bin.000010--slave_pass=xxx SatMay2215:43:022021-[info]Connectingtoroot@10.0.0.28(10.0.0.28:22).. Checkingslaverecoveryenvironmentsettings.. Relaylogfoundat/var/lib/mysql,uptoSlave2-relay-bin.000010 Temporaryrelaylogfileis/var/lib/mysql/Slave2-relay-bin.000010 Checkingifsuper_read_onlyisdefinedandturnedon..notpresentorturnedoff,ignoring. Testingmysqlconnectionandprivileges.. mysql:[Warning]Usingapasswordonthecommandlineinterfacecanbeinsecure. done. Testingmysqlbinlogoutput..done. Cleaninguptestfile(s)..done. SatMay2215:43:022021-[info]Slavessettingscheckdone. SatMay2215:43:022021-[info] 10.0.0.8(10.0.0.8:3306)(currentmaster) +--10.0.0.18(10.0.0.18:3306) +--10.0.0.28(10.0.0.28:3306) SatMay2215:43:022021-[info]Checkingreplicationhealthon10.0.0.18.. SatMay2215:43:022021-[info]ok. SatMay2215:43:022021-[info]Checkingreplicationhealthon10.0.0.28.. SatMay2215:43:022021-[info]ok. SatMay2215:43:022021-[info]Checkingmaster_ip_failover_scriptstatus: SatMay2215:43:022021-[info]/usr/local/bin/master_ip_failover--command=status--ssh_user=root--orig_master_host=10.0.0.8--orig_master_ip=10.0.0.8--orig_master_port=3306 INSCRIPTTEST====/sbin/ifconfigeth0:1down==/sbin/ifconfigeth0:110.0.0.100/24;/sbin/arping-Ieth0-c3-s10.0.0.100/2410.0.0.254>/dev/null2>&1=== CheckingtheStatusofthescript..OK SatMay2215:43:032021-[info]OK. SatMay2215:43:032021-[warning]shutdown_scriptisnotdefined. SatMay2215:43:032021-[info]Gotexitcode0(Notmasterdead). MySQLReplicationHealthisOK. [root@MHA-Manager~]# 9.启动MHA 开启MHA,默认是前台运行,生产环境一般为后台执行 [root@MHA-Manager~]#nohupmasterha_manager--conf=/etc/mastermha/app1.conf&>/dev/null 查看状态 [root@MHA-Manager~]#masterha_check_status--conf=/etc/mastermha/app1.conf app1(pid:1321)isrunning(0:PING_OK),master:10.0.0.8 在Master服务器上进行健康性检查 [root@Master~]#tail-f/var/lib/mysql/Master.log 2021-05-22T08:08:15.494093Z24QuerySELECT1AsValue 2021-05-22T08:08:16.494128Z24QuerySELECT1AsValue 2021-05-22T08:08:17.496217Z24QuerySELECT1AsValue 2021-05-22T08:08:18.498052Z24QuerySELECT1AsValue 2021-05-22T08:08:19.500948Z24QuerySELECT1AsValue 2021-05-22T08:08:20.504335Z24QuerySELECT1AsValue 2021-05-22T08:08:21.507257Z24QuerySELECT1AsValue 2021-05-22T08:08:22.507209Z24QuerySELECT1AsValue 2021-05-22T08:08:23.507075Z24QuerySELECT1AsValue 2021-05-22T08:08:24.509524Z24QuerySELECT1AsValue 2021-05-22T08:08:25.510962Z24QuerySELECT1AsValue 2021-05-22T08:08:26.511363Z24QuerySELECT1AsValue 2021-05-22T08:08:27.513529Z24QuerySELECT1AsValue 10.模拟故障 停掉Master服务器的MySQL服务 [root@Master~]#systemctlstopmysqld.service 查看状态 [root@MHA-Manager~]#masterha_check_status--conf=/etc/mastermha/app1.conf app1isstopped(2:NOT_RUNNING). 验证VIP漂移至新的Master上 [root@Slave1~]#ifconfig eth0:flags=4163<UP,BROADCAST,RUNNING,MULTICAST>mtu1500 inet10.0.0.18netmask255.255.255.0broadcast10.0.0.255 inet6fe80::5a87:1464:beb7:38prefixlen64scopeid0x20<link> ether00:0c:29:9d:3e:68txqueuelen1000(Ethernet) RXpackets70084bytes88687133(84.5MiB) RXerrors0dropped0overruns0frame0 TXpackets35950bytes3857649(3.6MiB) TXerrors0dropped0overruns0carrier0collisions0 eth0:1:flags=4163<UP,BROADCAST,RUNNING,MULTICAST>mtu1500 inet10.0.0.100netmask255.255.255.0broadcast10.0.0.255 ether00:0c:29:9d:3e:68txqueuelen1000(Ethernet) lo:flags=73<UP,LOOPBACK,RUNNING>mtu65536 inet127.0.0.1netmask255.0.0.0 inet6::1prefixlen128scopeid0x10<host> looptxqueuelen1000(LocalLoopback) RXpackets920bytes136130(132.9KiB) RXerrors0dropped0overruns0frame0 TXpackets920bytes136130(132.9KiB) TXerrors0dropped0overruns0carrier0collisions0 [root@Slave1~]# 文章写得有不恰当之处,请多见谅。

优秀的个人博客,低调大师

Nginx+keepalived高可用配置实战

1、整体架构图如下 2、环境准备 今天所配置的是keepalived+nginx 的负载均衡 下载keepalived软件 [root@LB01 tools]# wget http://www.keepalived.org/software/keepalived-1.1.17.tar.gz 注意安装前检查内核的link文件 root@LB02 tools]# ll /usr/src/ total 8 drwxr-xr-x. 2 root root 4096 Sep 23 2011 debug drwxr-xr-x. 3 root root 4096 Oct 19 02:03 kernels lrwxrwxrwx. 1 root root 43 Oct 19 02:05 linux -> /usr/src/kernels/2.6.32-642.6.1.el6.x86_64/ 安装keepalived之前,安装几个依赖包 yum install openssl-devel -y yum install popt* -y 然后进行编译安装keepalived,前面介绍了安装过程,这里就不演示了 ./configure得出下面的结果 Keepalived configuration ------------------------ Keepalived version : 1.1.17 Compiler : gcc Compiler: -g -O2 Extra Lib: -lpopt -lssl -lcrypto Use IPVS Framework: Yes IPVS sync daemon support : Yes Use VRRP Framework : Yes Use LinkWatch: No Use Debug flags: No 注意./configure之后的结果,没有错误就可以了 make && make install 之后规范配置、启动文件路径 /bin/cp /usr/local/etc/rc.d/init.d/keepalived /etc/init.d/ /bin/cp /usr/local/etc/sysconfig/keepalived /etc/sysconfig/ mkdir /etc/keepalived -p /bin/cp /usr/local/etc/keepalived/keepalived.conf /etc/keepalived/ /bin/cp /usr/local/sbin/keepalived /usr/sbin/ /etc/init.d/keepalived start 注:nginx负载均衡相关配置请参考前面的文章 LNMP架构应用实战—Nginx反向代理负载均衡配置 3、实战配置keepalived [root@LB01 keepalived]# vi keepalived.conf ! Configuration File for keepalived global_defs { notification_email { abc@qq.com } notification_email_from Alexandre.Cassen@firewall.loc smtp_server 1.1.1.1 smtp_connect_timeout 30 router_id LVS_3 } vrrp_instance VI_1 { state MASTER interface eth0 virtual_router_id 19 priority 150 advert_int 1 authentication { auth_type PASS auth_pass 1111 } virtual_ipaddress { 192.168.1.254/24 } } [root@LB02 keepalived]# vi keepalived.conf ! Configuration File for keepalived global_defs { notification_email { abc@qq.com } notification_email_from Alexandre.Cassen@firewall.loc smtp_server 1.1.1.1 smtp_connect_timeout 30 router_id LVS_6 } vrrp_instance VI_1 { state BACKUP interface eth0 virtual_router_id 19 priority 100 advert_int 1 authentication { auth_type PASS auth_pass 1111 } virtual_ipaddress { 192.168.1.254/24 } } [root@LB01 keepalived]# /etc/init.d/keepalived start Starting keepalived:[ OK ] [root@LB02 keepalived]# /etc/init.d/keepalived start Starting keepalived[ OK ] [root@LB01 keepalived]# ip add|grep 192.168.1.254 inet 192.168.1.254/24 scope global secondary eth0 [root@LB02 keepalived]# ip add|grep 192.168.1.254 测试访问 表明可以正常切换 现在我们模拟keepalived主宕机,再测试 [root@LB01 conf]# /etc/init.d/keepalived stop Stopping keepalived: [ OK ] [root@LB02 ~]# ip add|grep 254 inet 192.168.1.254/24 scope global secondary eth0 4、反向代理服务故障自动切换 如果实际生产环境中当keeplived主的服务器nginx服务宕机,但是主又有VIP,这时就出现无法访问的现象,因此可以做如下的配置,使得这种情况可自已切换 vi check_nginx.sh #!/bin/sh white true do PNUM=`ps -ef|grep nginx|wc -l` #这里也可使用nmap 192.168.1.3 -p 80|grep open|wc -l来判断个数 if [ $PNUM -lt 3 ];then /etc/init.d/keepalived stop >/dec/null 2>&1 kill -9 keealived >/dec/null 2>&1 kill -9 keealived >/dec/null 2>&1 fi sleep 5 done sh check_nginx.sh & 启动个守护进程进行检查(或者加入定时任务定时执行检查),如果nginx服务出现故障,就立马停掉keepalived的服务,让它自动切换到备节点上去,这样就实现了自动切换的工作

优秀的个人博客,低调大师

高并发之——线程与多线程

一、线程与多线程 1.线程 在操作系统中,线程是比进程更小的能够独立运行的基本单位。同时,它也是CPU调度的基本单位。线程本身基本上不拥有系统资源,只是拥有一些在运行时需要用到的系统资源,例如程序计数器,寄存器和栈等。一个进程中的所有线程可以共享进程中的所有资源。 2.多线程 多线程可以理解为在同一个程序中能够同时运行多个不同的线程来执行不同的任务,这些线程可以同时利用CPU的多个核心运行。多线程编程能够最大限度的利用CPU的资源。如果某一个线程的处理不需要占用CPU资源时(例如IO线程),可以使当前线程让出CPU资源来让其他线程能够获取到CPU资源,进而能够执行其他线程对应的任务,达到最大化利用CPU资源的目的。 二、实现线程的方式 在Java中,实现线程的方式大体上分为三种,通过继承Thread类、实现Runnable接口,实现Callable接口。简单的示例代码分别如下所示。 继承Thread类代码 package io.binghe.concurrent.executor.test; /** * @author binghe * @version 1.0.0 * @description 继承Thread实现线程 */ public class ThreadTest extends Thread { @Override public void run() { //TODO 在此写在线程中执行的业务逻辑 } } 实现Runnable接口代码 package io.binghe.concurrent.executor.test; /** * @author binghe * @version 1.0.0 * @description 实现Runnable实现线程 */ public class RunnableTest implements Runnable { @Override public void run() { //TODO 在此写在线程中执行的业务逻辑 } } 实现Callable接口代码 package io.binghe.concurrent.executor.test; import java.util.concurrent.Callable; /** * @author binghe * @version 1.0.0 * @description 实现Callable实现线程 */ public class CallableTest implements Callable<String> { @Override public String call() throws Exception { //TODO 在此写在线程中执行的业务逻辑 return null; } } 三、线程的生命周期 1.生命周期 一个线程从创建,到最终的消亡,需要经历多种不同的状态,而这些不同的线程状态,由始至终也构成了线程生命周期的不同阶段。线程的生命周期可以总结为下图。 其中,几个重要的状态如下所示。 NEW:初始状态,线程被构建,但是还没有调用start()方法。 RUNNABLE:可运行状态,可运行状态可以包括:运行中状态和就绪状态。 BLOCKED:阻塞状态,处于这个状态的线程需要等待其他线程释放锁或者等待进入synchronized。 WAITING:表示等待状态,处于该状态的线程需要等待其他线程对其进行通知或中断等操作,进而进入下一个状态。 TIME_WAITING:超时等待状态。可以在一定的时间自行返回。 TERMINATED:终止状态,当前线程执行完毕。 2.代码示例 为了更好的理解线程的生命周期,以及生命周期中的各个状态,接下来使用代码示例来输出线程的每个状态信息。 WaitingTime 创建WaitingTime类,在while(true)循环中调用TimeUnit.SECONDS.sleep(long)方法来验证线程的TIMED_WARTING状态,代码如下所示。 package io.binghe.concurrent.executor.state; import java.util.concurrent.TimeUnit; /** * @author binghe * @version 1.0.0 * @description 线程不断休眠 */ public class WaitingTime implements Runnable{ @Override public void run() { while (true){ waitSecond(200); } } //线程等待多少秒 public static final void waitSecond(long seconds){ try { TimeUnit.SECONDS.sleep(seconds); } catch (InterruptedException e) { e.printStackTrace(); } } } WaitingState 创建WaitingState类,此线程会在一个while(true)循环中,获取当前类Class对象的synchronized锁,也就是说,这个类无论创建多少个实例,synchronized锁都是同一个,并且线程会处于等待状态。接下来,在synchronized中使用当前类的Class对象的wait()方法,来验证线程的WAITING状态,代码如下所示。 package io.binghe.concurrent.executor.state; /** * @author binghe * @version 1.0.0 * @description 线程在Warting上等待 */ public class WaitingState implements Runnable { @Override public void run() { while (true){ synchronized (WaitingState.class){ try { WaitingState.class.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } BlockedThread BlockedThread主要是在synchronized代码块中的while(true)循环中调用TimeUnit.SECONDS.sleep(long)方法来验证线程的BLOCKED状态。当启动两个BlockedThread线程时,首先启动的线程会处于TIMED_WAITING状态,后启动的线程会处于BLOCKED状态。代码如下所示。 package io.binghe.concurrent.executor.state; /** * @author binghe * @version 1.0.0 * @description 加锁后不再释放锁 */ public class BlockedThread implements Runnable { @Override public void run() { synchronized (BlockedThread.class){ while (true){ WaitingTime.waitSecond(100); } } } } ThreadState 启动各个线程,验证各个线程输出的状态,代码如下所示。 package io.binghe.concurrent.executor.state; /** * @author binghe * @version 1.0.0 * @description 线程的各种状态,测试线程的生命周期 */ public class ThreadState { public static void main(String[] args){ new Thread(new WaitingTime(), "WaitingTimeThread").start(); new Thread(new WaitingState(), "WaitingStateThread").start(); //BlockedThread-01线程会抢到锁,BlockedThread-02线程会阻塞 new Thread(new BlockedThread(), "BlockedThread-01").start(); new Thread(new BlockedThread(), "BlockedThread-02").start(); } } 运行ThreadState类,如下所示。 可以看到,未输出任何结果信息。可以在命令行输入“jps”命令来查看运行的Java进程。 c:\>jps 21584 Jps 17828 KotlinCompileDaemon 12284 Launcher 24572 28492 ThreadState 可以看到ThreadSate进程的进程号为28492,接下来,输入“jstack 28492”来查看ThreadSate进程栈的信息,如下所示。 c:\>jstack 28492 2020-02-15 00:27:08 Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.202-b08 mixed mode): "DestroyJavaVM" #16 prio=5 os_prio=0 tid=0x000000001ca05000 nid=0x1a4 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "BlockedThread-02" #15 prio=5 os_prio=0 tid=0x000000001ca04800 nid=0x6eb0 waiting for monitor entry [0x000000001da4f000] java.lang.Thread.State: BLOCKED (on object monitor) at io.binghe.concurrent.executor.state.BlockedThread.run(BlockedThread.java:28) - waiting to lock <0x0000000780a7e4e8> (a java.lang.Class for io.binghe.concurrent.executor.state.BlockedThread) at java.lang.Thread.run(Thread.java:748) "BlockedThread-01" #14 prio=5 os_prio=0 tid=0x000000001ca01800 nid=0x6e28 waiting on condition [0x000000001d94f000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at java.lang.Thread.sleep(Thread.java:340) at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386) at io.binghe.concurrent.executor.state.WaitingTime.waitSecond(WaitingTime.java:36) at io.binghe.concurrent.executor.state.BlockedThread.run(BlockedThread.java:28) - locked <0x0000000780a7e4e8> (a java.lang.Class for io.binghe.concurrent.executor.state.BlockedThread) at java.lang.Thread.run(Thread.java:748) "WaitingStateThread" #13 prio=5 os_prio=0 tid=0x000000001ca06000 nid=0x6fe4 in Object.wait() [0x000000001d84f000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x0000000780a7b488> (a java.lang.Class for io.binghe.concurrent.executor.state.WaitingState) at java.lang.Object.wait(Object.java:502) at io.binghe.concurrent.executor.state.WaitingState.run(WaitingState.java:29) - locked <0x0000000780a7b488> (a java.lang.Class for io.binghe.concurrent.executor.state.WaitingState) at java.lang.Thread.run(Thread.java:748) "WaitingTimeThread" #12 prio=5 os_prio=0 tid=0x000000001c9f8800 nid=0x3858 waiting on condition [0x000000001d74f000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at java.lang.Thread.sleep(Thread.java:340) at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386) at io.binghe.concurrent.executor.state.WaitingTime.waitSecond(WaitingTime.java:36) at io.binghe.concurrent.executor.state.WaitingTime.run(WaitingTime.java:29) at java.lang.Thread.run(Thread.java:748) "Service Thread" #11 daemon prio=9 os_prio=0 tid=0x000000001c935000 nid=0x6864 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C1 CompilerThread3" #10 daemon prio=9 os_prio=2 tid=0x000000001c88c800 nid=0x6a28 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C2 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x000000001c880000 nid=0x6498 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x000000001c87c000 nid=0x693c waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x000000001c87b800 nid=0x5d00 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x000000001c862000 nid=0x6034 runnable [0x000000001d04e000] java.lang.Thread.State: RUNNABLE at java.net.SocketInputStream.socketRead0(Native Method) at java.net.SocketInputStream.socketRead(SocketInputStream.java:116) at java.net.SocketInputStream.read(SocketInputStream.java:171) at java.net.SocketInputStream.read(SocketInputStream.java:141) at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284) at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326) at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178) - locked <0x0000000780b2fd88> (a java.io.InputStreamReader) at java.io.InputStreamReader.read(InputStreamReader.java:184) at java.io.BufferedReader.fill(BufferedReader.java:161) at java.io.BufferedReader.readLine(BufferedReader.java:324) - locked <0x0000000780b2fd88> (a java.io.InputStreamReader) at java.io.BufferedReader.readLine(BufferedReader.java:389) at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:64) "Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000000001c788800 nid=0x6794 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x000000001c7e3800 nid=0x3354 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000000001c771000 nid=0x6968 in Object.wait() [0x000000001cd4f000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x0000000780908ed0> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144) - locked <0x0000000780908ed0> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165) at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216) "Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x000000001c770800 nid=0x6590 in Object.wait() [0x000000001cc4f000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x0000000780906bf8> (a java.lang.ref.Reference$Lock) at java.lang.Object.wait(Object.java:502) at java.lang.ref.Reference.tryHandlePending(Reference.java:191) - locked <0x0000000780906bf8> (a java.lang.ref.Reference$Lock) at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153) "VM Thread" os_prio=2 tid=0x000000001a979800 nid=0x5c2c runnable "GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00000000033b9000 nid=0x4dc0 runnable "GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00000000033ba800 nid=0x6690 runnable "GC task thread#2 (ParallelGC)" os_prio=0 tid=0x00000000033bc000 nid=0x30b0 runnable "GC task thread#3 (ParallelGC)" os_prio=0 tid=0x00000000033be800 nid=0x6f68 runnable "GC task thread#4 (ParallelGC)" os_prio=0 tid=0x00000000033c1000 nid=0x6478 runnable "GC task thread#5 (ParallelGC)" os_prio=0 tid=0x00000000033c2000 nid=0x4fe4 runnable "GC task thread#6 (ParallelGC)" os_prio=0 tid=0x00000000033c5000 nid=0x584 runnable "GC task thread#7 (ParallelGC)" os_prio=0 tid=0x00000000033c6800 nid=0x6988 runnable "VM Periodic Task Thread" os_prio=2 tid=0x000000001c959800 nid=0x645c waiting on condition JNI global references: 12 由以上输出的信息可以看出:名称为WaitingTimeThread的线程处于TIMED_WAITING状态;名称为WaitingStateThread的线程处于WAITING状态;名称为BlockedThread-01的线程处于TIMED_WAITING状态;名称为BlockedThread-02的线程处于BLOCKED状态。 注意:使用jps结合jstack命令可以分析线上生产环境的Java进程的异常信息。 也可以直接点击IDEA下图所示的图表直接打印出线程的堆栈信息 输出的结果信息与使用“jstack 进程号”命令输出的信息基本一致。

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册