2024年8月

分为三步

  1. 启动类加
    @EnableAsync
    注解
  2. 在方法上加
    @Async
    注解
  3. 创建线程池配置类

1.启动类加
@EnableAsync
注解

@SpringBootApplication
@EnableAsync
public class FacadeH5Application {
    public static void main(String[] args) {
        SpringApplication.run(FacadeH5Application.class, args);
    }
}

2.在方法上加
@Async
注解

@Async
public void m1() {
    //do something
}

注意:导致
@Async
注解失效的几个原因

  1. 两个方法都在同一个类里面,一个方法调用另一个异步方法,不生效。但是如果在本类中注入自己的实例,再通过自己的实例调用异步方法就可行。
  2. @Async 方法所在的类没有交给 spring 代理(没加诸如@Component注解),不生效。
  3. 注解的方法不是是public方法,不生效。

3.创建线程池配置类

默认的线程池配置如下

# 核心线程数
spring.task.execution.pool.core-size=8  
# 最大线程数
spring.task.execution.pool.max-size=16
# 空闲线程存活时间
spring.task.execution.pool.keep-alive=60s
# 是否允许核心线程超时
spring.task.execution.pool.allow-core-thread-timeout=true
# 线程队列数量
spring.task.execution.pool.queue-capacity=100
# 线程关闭等待
spring.task.execution.shutdown.await-termination=false
spring.task.execution.shutdown.await-termination-period=
# 线程名称前缀
spring.task.execution.thread-name-prefix=task-

创建线程池配置类

@Configuration
public class ThreadPoolConfig {
    @Bean
    public TaskExecutor taskExecutor(){
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //设置核心线程数
        executor.setCorePoolSize(10);
        //设置最大线程数
        executor.setMaxPoolSize(20);
        //设置队列容量
        executor.setQueueCapacity(20);
        //设置线程活跃时间
        executor.setKeepAliveSeconds(30);
        //设置线程名称前缀
        executor.setThreadNamePrefix("sendSms-");
        //设置拒绝策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //等待所有任务结束后再关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        //设置线程池中任务的等待时间
        executor.setAwaitTerminationSeconds(60);

        return executor;
    }
}

配置多个线程池

有时候,一个项目中如果配置了多个线程池,那需要在 @Bean后面加上线程池的名称

@Configuration
public class ThreadPoolConfig {
    @Bean("ThreadPool1")
    public TaskExecutor taskExecutor(){
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
		......
        return executor;
    }
    @Bean("ThreadPool2")
    public TaskExecutor taskExecutor(){
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
		......
        return executor;
    }
}

在使用 @Async注解时就需要指明具体使用的线程池,如下格式

@Async("ThreadPool1")
public void m1() {
    //do something
}

前言

在现代 Web 开发中,WebAssembly (WASM) 已成为一种强大的工具。它使得开发者可以在浏览器中运行高性能的代码,跨越传统的 JavaScript 性能限制。Rust 语言因其高效性和内存安全性,成为了编写 WASM 模块的热门选择。本文将介绍如何将 Rust 代码编译为 WebAssembly,并在 Web 项目中使用。

1. 创建 Rust 项目

首先,我们需要创建一个新的 Rust 项目。由于我们要生成一个可以被其它语言或工具调用的模块,因此选择创建一个库项目,而不是可执行程序。使用
cargo
命令可以轻松完成:

cargo new lib_wasm --lib

这个命令会生成一个名为
lib_wasm
的项目,其中包含一个基础的
Cargo.toml
配置文件和一个
src/lib.rs
文件,你将在其中编写你的 Rust 代码。

2. 添加
wasm-bindgen
依赖项

在 Rust 中,
wasm-bindgen
是一个关键工具,它使 Rust 和 JavaScript 之间的交互变得更加简单。
wasm-bindgen
负责生成与 JavaScript 交互所需的绑定代码,让你能够直接调用 Rust 编写的函数。

要添加
wasm-bindgen
,你可以使用
cargo add
命令:

cargo add wasm-bindgen

或者,手动编辑
Cargo.toml
文件,添加如下依赖项:

[dependencies]
wasm-bindgen = "0.2"

添加
wasm-bindgen
后,Rust 编译器会在编译过程中生成必要的绑定文件,从而使你的 WASM 模块可以被 JavaScript 直接调用。

3. 安装
wasm32-unknown-unknown
目标

Rust 编译器默认会生成适用于本地机器架构的可执行文件。要编译成适用于 Web 的 WebAssembly 文件,我们需要添加一个特定的目标架构,即
wasm32-unknown-unknown
。这是一个通用的 WASM 目标,不依赖任何特定的操作系统。

使用以下命令安装该目标:

rustup target add wasm32-unknown-unknown

此命令会配置你的 Rust 工具链,使其能够生成适用于 WebAssembly 的二进制文件。

4. 编写 Rust 代码

现在,你可以在
src/lib.rs
文件中编写你希望导出的功能。例如,我们可以编写一个简单的函数,它接受一个名字作为参数并返回一个问候语:

use wasm_bindgen::prelude::*;

// 使用 #[wasm_bindgen] 宏来导出函数到 JavaScript
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

在这段代码中,我们使用了
#[wasm_bindgen]
宏将
greet
函数导出,使其可以从 JavaScript 中调用。

5. 编译 Rust 项目为 WASM

编写完代码后,我们可以将其编译为 WASM 文件。编译时指定目标为
wasm32-unknown-unknown
,并使用
--release
选项生成优化后的构建:

cargo build --target wasm32-unknown-unknown --release

编译完成后,生成的
.wasm
文件将存储在
target/wasm32-unknown-unknown/release/
目录下。

6. 使用
wasm-bindgen
生成 JavaScript 绑定代码

虽然编译生成了
.wasm
文件,但直接在 JavaScript 中使用它并不方便。为此,我们需要使用
wasm-bindgen
工具生成相应的 JavaScript 绑定代码。这将创建一个便于在 JavaScript 中调用的模块。

首先,确保已安装
wasm-bindgen-cli
工具。你可以通过以下命令安装:

cargo install wasm-bindgen-cli

然后,运行以下命令生成 JavaScript 绑定文件:

wasm-bindgen --out-dir ./out --target web target/wasm32-unknown-unknown/release/lib_wasm.wasm

这会在
out
目录中生成一系列文件,包括
.js
文件和
.wasm
文件,你可以直接在 Web 项目中使用。

7. 在网页中使用 WASM 模块

现在,生成的 WASM 模块已经可以在 Web 项目中使用。你只需在 HTML 文件中导入生成的 JavaScript 绑定文件,并调用 Rust 导出的函数。例如:

<!DOCTYPE html>
<html>
<head>
    <title>Lib WASM Demo</title>
</head>
<body>
    <script type="module">
        import init, { greet } from "./out/lib_wasm.js";
        init().then(() => {
            console.log(greet("World"));
        });
    </script>
</body>
</html>

这个示例会在控制台打印出 "Hello, World!"。其中,
init
函数用于初始化 WASM 模块,而
greet
函数则调用了我们在 Rust 中定义的函数。

web 项目目录结构如下:

index.html
out/
    ├── lib_wasm_bg.wasm
    ├── lib_wasm_bg.wasm.d.ts
    ├── lib_wasm.d.ts
    └── lib_wasm.js

out
目录中包含了生成的 WASM 文件以及相应的 JavaScript 绑定文件,
index.html
是一个简单的网页,用于测试和展示你的 WASM 模块。

结语

通过这套流程,你可以轻松地将 Rust 代码编译为 WebAssembly,并将其集成到 Web 项目中。Rust 的高效性和 WebAssembly 的灵活性相结合,可以为 Web 应用带来显著的性能提升。

volatile 是并发编程中的重要关键字,它的名气甚至是可以与 synchronized、ReentrantLock 等齐名,也是属于并发编程五杰之一。

需要注意的是 volatile 并不能保证原子性,因此
使用 volatile 并没有办法保证线程安全

并发编程五杰:
image.png

PS:“并发编程五杰”是我个人起的名字,大家也不用太当真。

1.什么是 volatile?

volatile 是 Java 中的一个关键字,用于修饰变量,它的主要作用是
保证变量的可见性和禁止指令重排序

  • 可见性
    :是指当一个线程修改了一个被 volatile 修饰的变量时,其他线程能够立即看到这个修改。
  • 禁止指令重排序
    :则是确保对 volatile 变量的读写操作不会被编译器或处理器随意重新排序,从而保证了程序执行的顺序符合我们的预期。

2.v
olatile 工作原理

为了实现可见性,Java 内存模型(JMM)会在对 volatile 变量进行写操作时,强制将工作内存中的值刷新到主内存,并在读取时强制从主内存中重新获取最新的值。

而禁止指令重排序是通过在编译器和处理器层面添加特定的内存屏障指令来实现的。

具体来说。

2.1 可见性实现原理

可见性
:在计算机编程特别是多线程编程中,“可见性”指的是一个线程对共享变量的修改,对于其他线程是否能够及时地、准确地“可见”,即其他线程是否能够及时感知到这个修改并获取到最新的值。

例如,在一个多线程环境中,如果线程 A 修改了一个共享变量的值,而线程 B 无法立即看到这个修改,那么就存在可见性问题。

多线程操作共享变量流程如下:

volatile 是通过
内存屏障(Memory Barrier)
来确保可见性。

  • 写屏障(Store Barrier)
    :在 volatile 变量的写操作之后插入写屏障,确保所有之前的写操作都同步到主内存中,从而使得其他线程在读取该变量时能够获取到最新的值。
  • 读屏障(Load Barrier)
    :在 volatile 变量的读操作之前插入读屏障,确保所有之前的写操作都已完成,从而读取到的是最新的值。

通过这种方式,volatile 变量在多线程环境下的读写操作能够保持较高的可见性,但需要注意的是,volatile 并不保证操作的原子性。

具体来说,volatile 内存可见性主要通过 lock 前缀指令实现的,它会锁定当前内存区域的缓存(缓存行),并且立即将当前缓存行数据写入主内存(耗时非常短),回写主内存的时候会通知其他线程缓存了该变量的地址失效,从而导致其他线程需要重新去主内存中重新读取数据到其工作线程中。

2.2 有序性实现原理

volatile 的有序性是通过插入内存屏障,在内存屏障前后禁止重排序优化,以此实现有序性的。

2.3 正确理解“内存屏障”?

volatile 保证可见性的“内存屏障”和保证有序性的“内存屏障”有什么区别呢?

在说它们的区别之前,我们现需要对“内存屏障”有一个大致的理解。

内存屏障,简单来说,就像是在内存操作中的一道“关卡”或者“栅栏”。

想象一下,计算机在执行程序的时候,为了提高效率,可能会对指令的执行顺序进行一些调整。但是在多线程或者多核心的环境下,这种随意的调整可能会导致一些问题。

内存屏障的作用就是阻止这种随意的调整,确保特定的内存操作按照我们期望的顺序执行。

所以“内存屏障”本身只是一种“技术”,而这种“技术”可以实现很多“业务功能”。

这就像 Spring 中的 AOP 一样,AOP 是一种“技术”,而这种技术可以实现很多业务功能。例如,针对日志处理可以使用 AOP、针对用户鉴权可以使用 AOP 等,而内存屏障也是一样,我们可以使用内存屏障实现可见性的“业务功能”,也可以实现有序性的“业务功能”等。

3.volatile 适用场景

volatile 常见场景有以下两种:

  1. 状态标记
  2. 单例模式中的双重检查锁

具体来说。

3.1 状态标记

例如,在多线程环境中用于表示某个任务是否完成的标志变量,具体代码如下:

volatile boolean isTaskFinished = false;

3.2 单例模式中的双重检查锁

class Singleton {
    private volatile static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

4.volatile 局限性

volatile 并
不能保证原子性,也就是并不能保证线程安全

例如,对于 i++ 这样的操作,它不是一个原子操作,单纯使用 volatile 修饰 i 并不能保证线程安全。

课后思考

为什么双重效验锁一定要加 volatile?不是已经加锁了吗?

本文已收录到我的面试小站
www.javacn.site
,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。

部署组件

该 Kubernetes 部署过程中,对于部署环节,涉及多个组件,主要有 kubeadm 、kubelet 、kubectl。

kubeadm介绍

Kubeadm 为构建 Kubernetes 提供了便捷、高效的“最佳实践” ,该工具提供了初始化完整 Kubernetes 过程所需的组件,其主要命令及功能有:

  • kubeadm init:用于搭建 Kubernetes 控制平面节点;
  • kubeadm join:用于搭建 Kubernetes 工作节点并将其加入到集群中;
  • kubeadm upgrade:用于升级 Kubernetes 集群到新版本;
  • kubeadm token:用于管理 kubeadm join 使用的 token;
  • kubeadm reset:用于恢复(重置)通过 kubeadm init 或者 kubeadm join 命令对节点进行的任何变更;
  • kubeadm certs:用于管理 Kubernetes 证书;
  • kubeadm kubeconfig:用于管理 kubeconfig 文件;
  • kubeadm version:用于显示(查询)kubeadm 的版本信息;
  • kubeadm alpha:用于预览当前从社区收集到的反馈中的 kubeadm 特性。

更多参考:
Kubeadm介绍

kubelet介绍

kubelet 是 Kubernetes 集群中用于操作 Docker 、containerd 等容器运行时的核心组件,需要在每个节点运行。通常该操作是基于 CRI 实现,kubelet 和 CRI 交互,以便于实现对 Kubernetes 的管控。

kubelet 主要用于配置容器网络、管理容器数据卷等容器全生命周期,对于 kubelet 而言,其主要的功能核心有:

  • Pod 更新事件;
  • Pod 生命周期管理;
  • 上报 Node 节点信息。

更多参考:
kubelet介绍

kubectl介绍

kubectl 控制 Kubernetes 集群管理器,是作为 Kubernetes 的命令行工具,用于与 apiserver 进行通信,使用 kubectl 工具在 Kubernetes 上部署和管理应用程序。
使用 kubectl,可以检查群集资源的创建、删除和更新组件。
同时集成了大量子命令,可更便捷的管理 Kubernetes 集群,主要命令如下:

  • Kubetcl -h:显示子命令;
  • kubectl option:查看全局选项;
  • kubectl <command> --help:查看子命令帮助信息;
  • kubelet [command] [PARAMS] -o=<format>:设置输出格式,如json、yaml等;
  • Kubetcl explain [RESOURCE]:查看资源的定义。

更多参考:
kubectl介绍

方案概述

方案介绍

本方案基于 kubeadm 部署工具实现完整生产环境可用的 Kubernetes 高可用集群,同时提供相关 Kubernetes 周边组件。
其主要信息如下:

  • 版本:Kubernetes 1.29.2 版本;
  • kubeadm:采用 kubeadm 部署Kubernetes;
  • OS:CentOS 8;
  • etcd:采用融合方式;
  • Nginx:以Pod形式运行与Kubernetes之上,即in Kubernetes模式,提供反向代理至3个master 6443端口;
  • KeepAlived:用于实现 apiserver 的高可用;
  • 其他主要部署组件包括:
    • Metrics:度量组件,用于提供相关监控指标;
    • Dashboard:Kubernetes 集群的前端图形界面;
    • Helm:Kubernetes Helm 包管理器工具,用于后续使用 helm 整合包快速部署应用;
    • Ingress:Kubernetes 服务暴露应用,用于提供7层的负载均衡,类似 Nginx,可建立外部和内部的多个映射规则;
    • containerd:Kubernetes底层容器时;
    • Longhorn:Kubernetes 动态存储组件,用于提供 Kubernetes 的持久存储。

提示:本方案部署所使用脚本均由本人提供,可能不定期更新。

部署规划

节点规划

节点主机名 IP 类型 运行服务
master01 172.24.10.11 Kubernetes master节点 kubeadm、kubelet、kubectl、KeepAlived、
containerd、etcd、kube-apiserver、kube-scheduler、
kube-controller-manager、calico、WebUI、metrics、ingress、Longhorn ui节点
master02 172.24.10.12 Kubernetes master节点 kubeadm、kubelet、kubectl、KeepAlived、
containerd、etcd、kube-apiserver、kube-scheduler、
kube-controller-manager、calico、WebUI、metrics、ingress、Longhorn ui节点
master03 172.24.10.13 Kubernetes master节点 kubeadm、kubelet、kubectl、KeepAlived、
containerd、etcd、kube-apiserver、kube-scheduler、
kube-controller-manager、calico、WebUI、metrics、ingress、Longhorn ui节点
worker01 172.24.10.14 Kubernetes worker节点 kubelet、containerd、calico、Longhorn存储节点
worker02 172.24.10.15 Kubernetes worker节点 kubelet、containerd、calico、Longhorn存储节点
worker03 172.24.10.16 Kubernetes worker节点 kubelet、containerd、calico、Longhorn存储节点
worker04 172.24.10.17 Kubernetes worker节点 kubelet、containerd、calico、Longhorn存储节点

Kubernetes集群高可用主要指的是控制平面的高可用,多个Master节点组件(通常为奇数)和Etcd组件的高可用,worker节点通过前端负载均衡VIP连接到Master。

架构图

Kubernetes高可用架构中etcd与Master节点组件混合部署方式特点:

  • 所需服务器节点资源少,具备超融合架构特点
  • 部署简单,利于管理
  • 容易进行横向扩展
  • etcd复用Kubernetes的高可用
  • 存在一定风险,如一台master主机挂了,master和etcd都少了一个节点,集群冗余度受到一定影响

提示:本实验使用Keepalived+Nginx架构实现Kubernetes的高可用。

主机名配置

需要对所有节点主机名进行相应配置。

[root@localhost ~]# hostnamectl set-hostname master01	    #其他节点依次修改

生产环境通常建议在内网部署dns服务器,使用dns服务器进行解析,本指南采用本地hosts文件名进行解析。
如下hosts文件修改仅需在master01执行,后续使用批量分发至其他所有节点。

[root@master01 ~]# cat >> /etc/hosts << EOF
172.24.10.11 master01
172.24.10.12 master02
172.24.10.13 master03
172.24.10.14 worker01
172.24.10.15 worker02
172.24.10.16 worker03
EOF

变量准备

为实现自动化部署,自动化分发相关文件,提前定义相关主机名、IP组、变量等。

[root@master01 ~]# wget http://down.linuxsb.com/mydeploy/k8s/v1.30.3/environment.sh

[root@master01 ~]# vi environment.sh            #确认相关主机名和IP
#!/bin/sh
#****************************************************************#
# ScriptName: environment.sh
# Author: xhy
# Create Date: 2022-10-11 17:10
# Modify Author: xhy
# Modify Date: 2023-11-30 23:00
# Version: v1
#***************************************************************#

# 集群 MASTER 机器 IP 数组
export MASTER_IPS=(172.24.10.11 172.24.10.12 172.24.10.13)

# 集群 MASTER IP 对应的主机名数组
export MASTER_NAMES=(master01 master02 master03)

# 集群 NODE 机器 IP 数组
export NODE_IPS=(172.24.10.14 172.24.10.15 172.24.10.16)

# 集群 NODE IP 对应的主机名数组
export NODE_NAMES=(worker01 worker02 worker03)

# 集群所有机器 IP 数组
export ALL_IPS=(172.24.10.11 172.24.10.12 172.24.10.13 172.24.10.14 172.24.10.15 172.24.10.16)

# 集群所有IP 对应的主机名数组
export ALL_NAMES=(master01 master02 master03 worker01 worker02 worker03)

互信配置

为了方便远程分发文件和执行命令,本方案配置master01节点到其它节点的 ssh信任关系,即免秘钥管理所有其他节点。

[root@master01 ~]# source environment.sh                                #载入变量
    
[root@master01 ~]# ssh-keygen -f ~/.ssh/id_rsa -N ''
[root@master01 ~]# for all_ip in ${ALL_IPS[@]}
  do
    echo ">>> ${all_ip}"
    ssh-copy-id -i ~/.ssh/id_rsa.pub root@${all_ip}
  done
  
[root@master01 ~]# for all_name in ${ALL_NAMES[@]}
  do
    echo ">>> ${all_name}"
    ssh-copy-id -i ~/.ssh/id_rsa.pub root@${all_name}
  done

提示:此操作仅需要在master01节点操作。

环境初始化

kubeadm本身仅用于部署Kubernetes集群,在正式使用kubeadm部署Kubernetes集群之前需要对操作系统环境进行准备,即环境初始化准备。
环境的初始化准备本方案使用脚本自动完成。
使用如下脚本对基础环境进行初始化,主要功能包括:

  • 安装containerd,Kubernetes平台底层的容器组件
  • 关闭SELinux及防火墙
  • 优化相关内核参数,针对生产环境Kubernetes集群的基础系统调优配置
  • 关闭swap
  • 设置相关模块,主要为转发模块
  • 配置相关基础软件,部署Kubernetes集群所需要的基础依赖包
[root@master01 ~]# wget http://down.linuxsb.com/mydeploy/k8s/v1.30.3/k8sconinit.sh

[root@master01 ~]# vim k8sconinit.sh
#!/bin/sh
#****************************************************************#
# ScriptName: k8sconinit.sh
# Author: xhy
# Create Date: 2020-05-30 16:30
# Modify Author: xhy
# Modify Date: 2024-02-28 22:38
# Version: v1
#***************************************************************#
# Initialize the machine. This needs to be executed on every machine.
rm -f /var/lib/rpm/__db.00*
rpm -vv --rebuilddb
#yum clean all 
#yum makecache
sleep 3s
# Install containerd
CONVERSION=1.6.32
yum -y install yum-utils device-mapper-persistent-data lvm2
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
sudo sed -i 's+download.docker.com+mirrors.aliyun.com/docker-ce+' /etc/yum.repos.d/docker-ce.repo
sleep 3s
yum -y install containerd.io-${CONVERSION}
mkdir /etc/containerd

cat > /etc/containerd/config.toml <<EOF
disabled_plugins = ["restart"]

[plugins.linux]
shim_debug = true

[plugin."io.containerd.grpc.v1.cri".registry.mirrors]
  [plugin."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
        endpoint = ["https://dbzucv6w.mirror.aliyuncs.com"]

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = true

[plugins.cri]
sandbox_image = "registry.k8s.io/pause:3.9"
EOF

cat > /etc/crictl.yaml <<EOF
runtime-endpoint: unix:///run/containerd/containerd.sock
image-endpoint: unix:///run/containerd/containerd.sock
timeout: 10
debug: false
EOF

systemctl restart containerd
systemctl enable containerd --now
systemctl status containerd

# Disable the SELinux.
sed -i 's/^SELINUX=.*/SELINUX=disabled/' /etc/selinux/config

# Turn off and disable the firewalld.
systemctl stop firewalld
systemctl disable firewalld

# Modify related kernel parameters & Disable the swap.
cat > /etc/sysctl.d/k8s.conf << EOF
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.tcp_tw_recycle = 0
vm.swappiness = 0
vm.overcommit_memory = 1
vm.panic_on_oom = 0
net.ipv6.conf.all.disable_ipv6 = 1
EOF
sysctl -p /etc/sysctl.d/k8s.conf >&/dev/null
swapoff -a
sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab
modprobe br_netfilter
modprobe overlay

# Add ipvs modules
cat > /etc/sysconfig/modules/ipvs.modules <<EOF
#!/bin/bash
modprobe -- ip_vs
modprobe -- ip_vs_rr
modprobe -- ip_vs_wrr
modprobe -- ip_vs_sh
modprobe -- nf_conntrack_ipv4
modprobe -- nf_conntrack
modprobe -- br_netfilter
modprobe -- overlay
EOF

chmod 755 /etc/sysconfig/modules/ipvs.modules
bash /etc/sysconfig/modules/ipvs.modules

# Install rpm
yum install -y conntrack ipvsadm ipset jq iptables curl sysstat libseccomp wget iproute-tc

# Update kernel
# rpm --import http://down.linuxsb.com/RPM-GPG-KEY-elrepo.org
# rpm -Uvh http://down.linuxsb.com/elrepo-release-7.el7.elrepo.noarch.rpm
# mv -b /etc/yum.repos.d/elrepo.repo /etc/yum.repos.d/backup
# wget -c http://down.linuxsb.com/myoptions/elrepo7.repo -O /etc/yum.repos.d/elrepo.repo 
# yum --disablerepo="*" --enablerepo="elrepo-kernel" install -y kernel-ml
# sed -i 's/^GRUB_DEFAULT=.*/GRUB_DEFAULT=0/' /etc/default/grub
# grub2-mkconfig -o /boot/grub2/grub.cfg
# yum -y --exclude=docker* update

# Reboot the machine.
# reboot

提示:此操作仅需要在master01节点操作。

  • 对于某些特性,可能需要升级内核,内核升级操作见
    018.Linux升级内核
  • 4.19版及以上内核nf_conntrack_ipv4已经改为nf_conntrack。
[root@master01 ~]# source environment.sh
[root@master01 ~]# chmod +x *.sh
[root@master01 ~]# for all_ip in ${ALL_IPS[@]}
  do
    echo ">>> ${all_ip}"
    scp -rp /etc/hosts root@${all_ip}:/etc/hosts
    scp -rp k8sconinit.sh root@${all_ip}:/root/
    ssh root@${all_ip} "bash /root/k8sconinit.sh"
  done

部署高可用组件

HAProxy安装

HAProxy是可提供高可用性、负载均衡以及基于TCP(从而可以反向代理kubeapiserver等应用)和HTTP应用的代理,支持虚拟主机,它是免费、快速并且可靠的一种高可用解决方案。

[root@master01 ~]# wget https://mirrors.huaweicloud.com/haproxy/3.0/src/haproxy-3.0.3.tar.gz

[root@master01 ~]# for master_ip in ${MASTER_IPS[@]}
  do
    echo ">>> ${master_ip}"
    ssh root@${master_ip} "yum -y install gcc gcc-c++ make libnl3 libnl3-devel libnfnetlink openssl-devel wget openssh-clients systemd-devel zlib-devel pcre-devel"
    scp -rp haproxy-3.0.3.tar.gz root@${master_ip}:/root/
    ssh root@${master_ip} "tar -zxvf haproxy-3.0.3.tar.gz"
    ssh root@${master_ip} "cd haproxy-3.0.3/ && make ARCH=x86_64 TARGET=linux-glibc USE_PCRE=1 USE_ZLIB=1 USE_SYSTEMD=1 PREFIX=/usr/local/haprpxy && make install PREFIX=/usr/local/haproxy"
    ssh root@${master_ip} "cp /usr/local/haproxy/sbin/haproxy /usr/sbin/"
    ssh root@${master_ip} "useradd -r haproxy && usermod -G haproxy haproxy"
    ssh root@${master_ip} "mkdir -p /etc/haproxy && mkdir -p /etc/haproxy/conf.d && cp -r /root/haproxy-3.0.3/examples/errorfiles/ /usr/local/haproxy/"
  done

提示:Haproxy官方参考: https://docs.haproxy.org/

KeepAlived安装

KeepAlived 是一个基于VRRP协议来实现的LVS服务高可用方案,可以解决静态路由出现的单点故障问题。
本方案3台master节点均部署并运行Keepalived,一台为主服务器(MASTER),另外两台为备份服务器(BACKUP)。
Master集群外表现为一个VIP,主服务器会发送特定的消息给备份服务器,当备份服务器收不到这个消息的时候,即主服务器宕机的时候,备份服务器就会接管虚拟IP,继续提供服务,从而保证了高可用性。

[root@master01 ~]# wget https://www.keepalived.org/software/keepalived-2.3.1.tar.gz
[root@master01 ~]# for master_ip in ${MASTER_IPS[@]}
  do
    echo ">>> ${master_ip}"
    ssh root@${master_ip} "yum -y install curl gcc gcc-c++ make libnl3 libnl3-devel libnfnetlink openssl-devel"
    scp -rp keepalived-2.3.1.tar.gz root@${master_ip}:/root/
    ssh root@${master_ip} "tar -zxvf keepalived-2.3.1.tar.gz"
    ssh root@${master_ip} "cd keepalived-2.3.1/ && LDFLAGS=\"$LDFAGS -L /usr/local/openssl/lib/\" ./configure --sysconf=/etc --prefix=/usr/local/keepalived && make && make install"
    ssh root@${master_ip} "systemctl enable keepalived"
  done

提示:如上仅需Master01节点操作,从而实现所有节点自动化安装。若出现如下报错:undefined reference to `OPENSSL_init_ssl’,可带上openssl lib路径:

LDFLAGS="$LDFAGS -L /usr/local/openssl/lib/" ./configure --sysconf=/etc --prefix=/usr/local/keepalived

提示:KeepAlive官方参考: https://www.keepalived.org/manpage.html

创建配置文件

创建集群部署所需的相关组件配置,采用脚本自动化创建相关配置文件。

[root@master01 ~]# wget http://down.linuxsb.com/mydeploy/k8s/v1.30.3/k8sconfig.sh				#拉取自动部署脚本

[root@master01 ~]# vim k8sconfig.sh
#!/bin/sh
#****************************************************************#
# ScriptName: k8sconfig
# Author: xhy
# Create Date: 2022-06-08 20:00
# Modify Author: xhy
# Modify Date: 2024-02-25 23:57
# Version: v3
#***************************************************************#

#######################################
# set variables below to create the config files, all files will create at ./kubeadm directory
#######################################

# master keepalived virtual ip address
export K8SHA_VIP=172.24.10.100

# master01 ip address
export K8SHA_IP1=172.24.10.11

# master02 ip address
export K8SHA_IP2=172.24.10.12

# master03 ip address
export K8SHA_IP3=172.24.10.13

# master01 hostname
export K8SHA_HOST1=master01

# master02 hostname
export K8SHA_HOST2=master02

# master03 hostname
export K8SHA_HOST3=master03

# master01 network interface name
export K8SHA_NETINF1=eth0

# master02 network interface name
export K8SHA_NETINF2=eth0

# master03 network interface name
export K8SHA_NETINF3=eth0

# keepalived auth_pass config
export K8SHA_KEEPALIVED_AUTH=412f7dc3bfed32194d1600c483e10ad1d

# kubernetes CIDR pod subnet
export K8SHA_PODCIDR=10.10.0.0/16

# kubernetes CIDR svc subnet
export K8SHA_SVCCIDR=10.20.0.0/16

# kubernetes CIDR pod mtu
export K8SHA_PODMTU=1450

##############################
# please do not modify anything below
##############################

mkdir -p kubeadm/$K8SHA_HOST1/{keepalived,haproxy}
mkdir -p kubeadm/$K8SHA_HOST2/{keepalived,haproxy}
mkdir -p kubeadm/$K8SHA_HOST3/{keepalived,haproxy}
mkdir -p kubeadm/keepalived
mkdir -p kubeadm/haproxy

echo "create directory files success."

# wget all files
wget -c -P kubeadm/keepalived/ http://down.linuxsb.com/mydeploy/k8s/common/k8s-keepalived.conf.tpl
wget -c -P kubeadm/keepalived/ http://down.linuxsb.com/mydeploy/k8s/common/check_apiserver.sh
wget -c -P kubeadm/haproxy/ http://down.linuxsb.com/mydeploy/k8s/common/k8s-haproxy.cfg.tpl
wget -c -P kubeadm/haproxy/ http://down.linuxsb.com/mydeploy/k8s/common/k8s-haproxy.service
wget -c -P kubeadm/ http://down.linuxsb.com/mydeploy/k8s/v1.30.3/kubeadm-config.yaml.tpl
wget -c -P kubeadm/calico/ http://down.linuxsb.com/mydeploy/k8s/calico/v3.28.1/calico.yaml.tpl
wget -c -P kubeadm/ http://down.linuxsb.com/mydeploy/k8s/v1.30.3/k8simage.sh

echo "down files success."

# create all kubeadm-config.yaml files
sed \
-e "s/K8SHA_HOST1/${K8SHA_HOST1}/g" \
-e "s/K8SHA_HOST2/${K8SHA_HOST2}/g" \
-e "s/K8SHA_HOST3/${K8SHA_HOST3}/g" \
-e "s/K8SHA_IP1/${K8SHA_IP1}/g" \
-e "s/K8SHA_IP2/${K8SHA_IP2}/g" \
-e "s/K8SHA_IP3/${K8SHA_IP3}/g" \
-e "s/K8SHA_VIP/${K8SHA_VIP}/g" \
-e "s!K8SHA_PODCIDR!${K8SHA_PODCIDR}!g" \
-e "s!K8SHA_SVCCIDR!${K8SHA_SVCCIDR}!g" \
kubeadm/kubeadm-config.yaml.tpl > kubeadm/kubeadm-config.yaml

echo "create kubeadm-config.yaml files success."

# create all keepalived files
chmod u+x kubeadm/keepalived/check_apiserver.sh
cp kubeadm/keepalived/check_apiserver.sh kubeadm/$K8SHA_HOST1/keepalived
cp kubeadm/keepalived/check_apiserver.sh kubeadm/$K8SHA_HOST2/keepalived
cp kubeadm/keepalived/check_apiserver.sh kubeadm/$K8SHA_HOST3/keepalived

sed \
-e "s/K8SHA_KA_STATE/BACKUP/g" \
-e "s/K8SHA_KA_INTF/${K8SHA_NETINF1}/g" \
-e "s/K8SHA_IPLOCAL/${K8SHA_IP1}/g" \
-e "s/K8SHA_KA_PRIO/102/g" \
-e "s/K8SHA_VIP/${K8SHA_VIP}/g" \
-e "s/K8SHA_KA_AUTH/${K8SHA_KEEPALIVED_AUTH}/g" \
kubeadm/keepalived/k8s-keepalived.conf.tpl > kubeadm/$K8SHA_HOST1/keepalived/keepalived.conf

sed \
-e "s/K8SHA_KA_STATE/BACKUP/g" \
-e "s/K8SHA_KA_INTF/${K8SHA_NETINF2}/g" \
-e "s/K8SHA_IPLOCAL/${K8SHA_IP2}/g" \
-e "s/K8SHA_KA_PRIO/101/g" \
-e "s/K8SHA_VIP/${K8SHA_VIP}/g" \
-e "s/K8SHA_KA_AUTH/${K8SHA_KEEPALIVED_AUTH}/g" \
kubeadm/keepalived/k8s-keepalived.conf.tpl > kubeadm/$K8SHA_HOST2/keepalived/keepalived.conf

sed \
-e "s/K8SHA_KA_STATE/BACKUP/g" \
-e "s/K8SHA_KA_INTF/${K8SHA_NETINF3}/g" \
-e "s/K8SHA_IPLOCAL/${K8SHA_IP3}/g" \
-e "s/K8SHA_KA_PRIO/100/g" \
-e "s/K8SHA_VIP/${K8SHA_VIP}/g" \
-e "s/K8SHA_KA_AUTH/${K8SHA_KEEPALIVED_AUTH}/g" \
kubeadm/keepalived/k8s-keepalived.conf.tpl > kubeadm/$K8SHA_HOST3/keepalived/keepalived.conf

echo "create keepalived files success. kubeadm/$K8SHA_HOST1/keepalived/"
echo "create keepalived files success. kubeadm/$K8SHA_HOST2/keepalived/"
echo "create keepalived files success. kubeadm/$K8SHA_HOST3/keepalived/"

# create all haproxy files
sed \
-e "s/K8SHA_IP1/$K8SHA_IP1/g" \
-e "s/K8SHA_IP2/$K8SHA_IP2/g" \
-e "s/K8SHA_IP3/$K8SHA_IP3/g" \
-e "s/K8SHA_HOST1/$K8SHA_HOST1/g" \
-e "s/K8SHA_HOST2/$K8SHA_HOST2/g" \
-e "s/K8SHA_HOST3/$K8SHA_HOST3/g" \
kubeadm/haproxy/k8s-haproxy.cfg.tpl > kubeadm/haproxy/haproxy.conf

echo "create haproxy files success. kubeadm/$K8SHA_HOST1/haproxy/"
echo "create haproxy files success. kubeadm/$K8SHA_HOST2/haproxy/"
echo "create haproxy files success. kubeadm/$K8SHA_HOST3/haproxy/"

# create calico yaml file
sed \
-e "s!K8SHA_PODCIDR!${K8SHA_PODCIDR}!g" \
-e "s!K8SHA_PODMTU!${K8SHA_PODMTU}!g" \
kubeadm/calico/calico.yaml.tpl > kubeadm/calico/calico.yaml

echo "create calico file success."

# scp all file
scp -rp kubeadm/haproxy/haproxy.conf root@$K8SHA_HOST1:/etc/haproxy/haproxy.cfg
scp -rp kubeadm/haproxy/haproxy.conf root@$K8SHA_HOST2:/etc/haproxy/haproxy.cfg
scp -rp kubeadm/haproxy/haproxy.conf root@$K8SHA_HOST3:/etc/haproxy/haproxy.cfg
scp -rp kubeadm/haproxy/k8s-haproxy.service root@$K8SHA_HOST1:/usr/lib/systemd/system/haproxy.service
scp -rp kubeadm/haproxy/k8s-haproxy.service root@$K8SHA_HOST2:/usr/lib/systemd/system/haproxy.service
scp -rp kubeadm/haproxy/k8s-haproxy.service root@$K8SHA_HOST3:/usr/lib/systemd/system/haproxy.service

scp -rp kubeadm/$K8SHA_HOST1/keepalived/* root@$K8SHA_HOST1:/etc/keepalived/
scp -rp kubeadm/$K8SHA_HOST2/keepalived/* root@$K8SHA_HOST2:/etc/keepalived/
scp -rp kubeadm/$K8SHA_HOST3/keepalived/* root@$K8SHA_HOST3:/etc/keepalived/

echo "scp haproxy & keepalived file success."

chmod u+x kubeadm/*.sh

[root@master01 ~]# bash k8sconfig.sh

解释:如上仅需Master01节点操作。执行k8sconfig.sh脚本后会生产如下配置文件清单:

  • kubeadm-config.yaml:kubeadm初始化配置文件,位于kubeadm/目录,可参考
    kubeadm 配置
  • keepalived:keepalived配置文件,位于各个master节点的/etc/keepalived目录
  • haproxy:haproxy的配置文件,位于各个master节点的/etc/haproxy/目录
  • calico.yaml:calico网络组件部署文件,位于kubeadm/calico/目录
[root@master01 ~]# vim kubeadm/kubeadm-config.yaml	#检查集群初始化配置
---
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
networking:
  serviceSubnet: "10.20.0.0/16"			     	    #设置svc网段
  podSubnet: "10.10.0.0/16"                         #设置Pod网段
  dnsDomain: "cluster.local"
kubernetesVersion: "v1.30.3"			    	    #设置安装版本
controlPlaneEndpoint: "172.24.10.100:16443"		    #设置相关API VIP地址
apiServer:
  certSANs:
  - 127.0.0.1
  - master01
  - master02
  - master03
  - 172.24.10.11
  - 172.24.10.12
  - 172.24.10.13
  - 172.24.10.100
  timeoutForControlPlane: 4m0s
certificatesDir: "/etc/kubernetes/pki"
imageRepository: "registry.k8s.io"
#clusterName: "example-cluster"

---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd

---
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: ipvs

提示:如上仅需Master01节点操作,更多config文件参考: kubeadm 配置 (v1beta3)
默认kubeadm配置可使用kubeadm config print init-defaults > config.yaml生成。

启动服务

启动keepalive和HAProxy服务,从而构建master节点的高可用。

  • 检查服务配置
[root@master01 ~]# cat /etc/keepalived/keepalived.conf              #所有节点确认相关keepalive配置文件
! Configuration File for keepalived
global_defs {
    router_id LVS_DEVEL
    script_user root
    enable_script_security
}
vrrp_script check_apiserver {
    script "/etc/keepalived/check_apiserver.sh"
    interval 5
    weight -60
    fall 2
    rise 2
}
vrrp_instance VI_1 {
    state BACKUP
    interface eth0
    mcast_src_ip 172.24.10.11
    virtual_router_id 51
    priority 102
    advert_int 5
    authentication {
        auth_type PASS
        auth_pass 412f7dc3bfed32194d1600c483e10ad1d
    }
    virtual_ipaddress {
        172.24.10.100
    }
    track_script {
       check_apiserver
    }
}
[root@master01 ~]# cat /etc/keepalived/check_apiserver.sh              #所有节点确认相关keepalive监测脚本文件
#!/bin/bash

# if check error then repeat check for 12 times, else exit
err=0
for k in $(seq 1 12)
do
    check_code=$(curl -k https://localhost:6443)
    if [[ $check_code == "" ]]; then
        err=$(expr $err + 1)
        sleep 5
        continue
    else
        err=0
        break
    fi
done

if [[ $err != "0" ]]; then
    # if apiserver is down send SIG=1
    echo 'apiserver error!'
    exit 1
else
    # if apiserver is up send SIG=0
    echo 'apiserver normal!'
    exit 0
fi
  • 启动高可用服务
[root@master01 ~]# for master_ip in ${MASTER_IPS[@]}
do
    echo ">>> ${master_ip}"
    ssh root@${master_ip} "systemctl enable haproxy.service --now && systemctl restart haproxy.service"
    ssh root@${master_ip} "systemctl enable keepalived.service --now && systemctl restart keepalived.service"
    ssh root@${master_ip} "systemctl status keepalived.service | grep Active"
    ssh root@${master_ip} "systemctl status haproxy.service | grep Active"
done
[root@master01 ~]# for all_ip in ${ALL_IPS[@]}
do
    echo ">>> ${all_ip}"
    ssh root@${all_ip} "ping -c1 172.24.10.100"
done								                #等待10s执行检查

提示:如上仅需Master01节点操作,从而实现所有节点自动启动服务。

集群部署

相关组件包

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

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

kubeadm不能安装或管理 kubelet 或 kubectl ,因此在初始化集群之前必须完成kubelet和kubectl的安装,且能保证他们满足通过 kubeadm 安装的 Kubernetes控制层对版本的要求。
如果版本没有满足匹配要求,可能导致一些意外错误或问题。
具体相关组件安装见;
附001.kubectl介绍及使用书

提示:Kubernetes 1.29.2版本所有兼容相应组件的版本参考: https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/CHANGELOG-1.29.md

正式安装

快速安装所有节点的kubeadm、kubelet、kubectl组件。

[root@master01 ~]# for all_ip in ${ALL_IPS[@]}
  do
    echo ">>> ${all_ip}"
    ssh root@${all_ip} "cat <<EOF | sudo tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.30/rpm/
enabled=1
gpgcheck=1
gpgkey=https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.30/rpm/repodata/repomd.xml.key
EOF"
    ssh root@${all_ip} "yum install -y kubelet-1.30.3-150500.1.1 kubectl-1.30.3-150500.1.1 --disableexcludes=kubernetes"
    ssh root@${all_ip} "yum install -y kubeadm-1.30.3-150500.1.1 --disableexcludes=kubernetes"
    ssh root@${all_ip} "systemctl enable kubelet"
done
[root@master01 ~]# yum search -y kubelet --showduplicates             #查看相应版本 

提示:如上仅需Master01节点操作,从而实现所有节点自动化安装,同时此时不需要启动kubelet,初始化的过程中会自动启动的,如果此时启动了会出现报错,忽略即可。

说明:同时安装了cri-tools, kubernetes-cni, socat三个依赖:
socat:kubelet的依赖;
cri-tools:即CRI(Container Runtime Interface)容器运行时接口的命令行工具。

集群初始化

拉取镜像

初始化过程中会pull大量镜像,并且镜像位于国外,可能出现无法pull的情况导致Kubernetes初始化失败。建议提前准备镜像,保证后续初始化。

[root@master01 ~]# kubeadm --kubernetes-version=v1.30.3 config images list     	#列出所需镜像

[root@master01 ~]# vim kubeadm/k8simage.sh
#!/bin/sh
#***************************************************************#
# ScriptName: v1.30.3/k8simage.sh
# Author: xhy
# Create Date: 2024-08-08 22:00
# Modify Author: xhy
# Modify Date: 2024-08-08 22:00
# Version: v1
#***************************************************************#

KUBE_VERSION=v1.30.3
KUBE_PAUSE_VERSION=3.9
ETCD_VERSION=3.5.12-0
CORE_DNS_VERSION=v1.11.1
K8S_URL=registry.k8s.io
UCLOUD_URL=uhub.service.ucloud.cn/imxhy
LONGHORN_URL=longhornio
CALICO_URL='docker.io/calico'
CALICO_VERSION=v3.28.1
METRICS_SERVER_VERSION=v0.7.1
INGRESS_VERSION=v1.11.1
INGRESS_WEBHOOK_VERSION=v1.4.1
LONGHORN_VERSION=v1.6.2
LONGHORN_VERSION2=v0.0.37
CSI_ATTACHER_VERSION=v4.5.1
CSI_NODE_DRIVER_VERSION=v2.9.2
CSI_PROVISIONER_VERSION=v3.6.4
CSI_RESIZER_VERSION=v1.10.1
CSI_SNAP_VERSION=v6.3.4
CSI_LIVE_VERSION=v2.12.0

mkdir -p k8simages/

# config node hostname
export ALL_IPS=(master02 master03 worker01 worker02 worker03)

kubeimages=(kube-proxy:${KUBE_VERSION}
kube-scheduler:${KUBE_VERSION}
kube-controller-manager:${KUBE_VERSION}
kube-apiserver:${KUBE_VERSION}
pause:${KUBE_PAUSE_VERSION}
etcd:${ETCD_VERSION}
)

for kubeimageName in ${kubeimages[@]} ; do
echo ${kubeimageName}
ctr -n k8s.io images pull ${UCLOUD_URL}/${kubeimageName}
ctr -n k8s.io images tag ${UCLOUD_URL}/${kubeimageName} ${K8S_URL}/${kubeimageName}
ctr -n k8s.io images rm ${UCLOUD_URL}/${kubeimageName}
ctr -n k8s.io images export k8simages/${kubeimageName}\.tar ${K8S_URL}/${kubeimageName}
done

corednsimages=(coredns:${CORE_DNS_VERSION}
)

for corednsimageName in ${corednsimages[@]} ; do
echo ${corednsimageName}
ctr -n k8s.io images pull ${UCLOUD_URL}/${corednsimageName}
ctr -n k8s.io images tag ${UCLOUD_URL}/${corednsimageName} ${K8S_URL}/coredns/${corednsimageName}
ctr -n k8s.io images rm ${UCLOUD_URL}/${corednsimageName}
ctr -n k8s.io images export k8simages/${corednsimageName}\.tar ${K8S_URL}/coredns/${corednsimageName}
done

calimages=(cni:${CALICO_VERSION}
node:${CALICO_VERSION}
kube-controllers:${CALICO_VERSION})

for calimageName in ${calimages[@]} ; do
echo ${calimageName}
ctr -n k8s.io images pull ${UCLOUD_URL}/${calimageName}
ctr -n k8s.io images tag ${UCLOUD_URL}/${calimageName} ${CALICO_URL}/${calimageName}
ctr -n k8s.io images rm ${UCLOUD_URL}/${calimageName}
ctr -n k8s.io images export k8simages/${calimageName}\.tar ${CALICO_URL}/${calimageName}
done

metricsimages=(metrics-server:${METRICS_SERVER_VERSION})

for metricsimageName in ${metricsimages[@]} ; do
echo ${metricsimageName}
ctr -n k8s.io images pull ${UCLOUD_URL}/${metricsimageName}
ctr -n k8s.io images tag ${UCLOUD_URL}/${metricsimageName} ${K8S_URL}/metrics-server/${metricsimageName}
ctr -n k8s.io images rm ${UCLOUD_URL}/${metricsimageName}
ctr -n k8s.io images export k8simages/${metricsimageName}\.tar ${K8S_URL}/metrics-server/${metricsimageName}
done

ingressimages=(controller:${INGRESS_VERSION}
kube-webhook-certgen:${INGRESS_WEBHOOK_VERSION}
)

for ingressimageName in ${ingressimages[@]} ; do
echo ${ingressimageName}
ctr -n k8s.io images pull ${UCLOUD_URL}/${ingressimageName}
ctr -n k8s.io images tag ${UCLOUD_URL}/${ingressimageName} ${K8S_URL}/ingress-nginx/${ingressimageName}
ctr -n k8s.io images rm ${UCLOUD_URL}/${ingressimageName}
ctr -n k8s.io images export k8simages/${ingressimageName}\.tar ${K8S_URL}/ingress-nginx/${ingressimageName}
done

longhornimages01=(longhorn-engine:${LONGHORN_VERSION}
longhorn-instance-manager:${LONGHORN_VERSION}
longhorn-manager:${LONGHORN_VERSION}
longhorn-ui:${LONGHORN_VERSION}
backing-image-manager:${LONGHORN_VERSION}
longhorn-share-manager:${LONGHORN_VERSION}
)

for longhornimageNameA in ${longhornimages01[@]} ; do
echo ${longhornimageNameA}
ctr -n k8s.io images pull ${UCLOUD_URL}/${longhornimageNameA}
ctr -n k8s.io images tag ${UCLOUD_URL}/${longhornimageNameA} ${LONGHORN_URL}/${longhornimageNameA}
ctr -n k8s.io images rm ${UCLOUD_URL}/${longhornimageNameA}
ctr -n k8s.io images export k8simages/${longhornimageNameA}\.tar ${LONGHORN_URL}/${longhornimageNameA}
done

longhornimages02=(support-bundle-kit:${LONGHORN_VERSION2})

for longhornimageNameB in ${longhornimages02[@]} ; do
echo ${longhornimageNameB}
ctr -n k8s.io images pull ${UCLOUD_URL}/${longhornimageNameB}
ctr -n k8s.io images tag ${UCLOUD_URL}/${longhornimageNameB} ${LONGHORN_URL}/${longhornimageNameB}
ctr -n k8s.io images rm ${UCLOUD_URL}/${longhornimageNameB}
ctr -n k8s.io images export k8simages/${longhornimageNameB}\.tar ${LONGHORN_URL}/${longhornimageNameB}
done

csiimages=(csi-attacher:${CSI_ATTACHER_VERSION}
csi-node-driver-registrar:${CSI_NODE_DRIVER_VERSION}
csi-provisioner:${CSI_PROVISIONER_VERSION}
csi-resizer:${CSI_RESIZER_VERSION}
csi-snapshotter:${CSI_SNAP_VERSION}
livenessprobe:${CSI_LIVE_VERSION}
)

for csiimageName in ${csiimages[@]} ; do
echo ${csiimageName}
ctr -n k8s.io images pull ${UCLOUD_URL}/${csiimageName}
ctr -n k8s.io images tag ${UCLOUD_URL}/${csiimageName} ${LONGHORN_URL}/${csiimageName}
ctr -n k8s.io images rm ${UCLOUD_URL}/${csiimageName}
ctr -n k8s.io images export k8simages/${csiimageName}\.tar ${LONGHORN_URL}/${csiimageName}
done

allimages=(kube-proxy:${KUBE_VERSION}
kube-scheduler:${KUBE_VERSION}
kube-controller-manager:${KUBE_VERSION}
kube-apiserver:${KUBE_VERSION}
pause:${KUBE_PAUSE_VERSION}
etcd:${ETCD_VERSION}
coredns:${CORE_DNS_VERSION}
cni:${CALICO_VERSION}
node:${CALICO_VERSION}
kube-controllers:${CALICO_VERSION}
metrics-server:${METRICS_SERVER_VERSION}
controller:${INGRESS_VERSION}
kube-webhook-certgen:${INGRESS_WEBHOOK_VERSION}
longhorn-engine:${LONGHORN_VERSION}
longhorn-instance-manager:${LONGHORN_VERSION}
longhorn-manager:${LONGHORN_VERSION}
longhorn-ui:${LONGHORN_VERSION}
backing-image-manager:${LONGHORN_VERSION}
longhorn-share-manager:${LONGHORN_VERSION}
support-bundle-kit:${LONGHORN_VERSION2}
csi-attacher:${CSI_ATTACHER_VERSION}
csi-node-driver-registrar:${CSI_NODE_DRIVER_VERSION}
csi-provisioner:${CSI_PROVISIONER_VERSION}
csi-resizer:${CSI_RESIZER_VERSION}
csi-snapshotter:${CSI_SNAP_VERSION}
livenessprobe:${CSI_LIVE_VERSION}
)
for all_ip in ${ALL_IPS[@]}
  do  
    echo ">>> ${all_ip}"
    ssh root@${all_ip} "mkdir /root/k8simages"
    scp -rp k8simages/* root@${all_ip}:/root/k8simages/
  done

for allimageName in ${allimages[@]}
  do
  for all_ip in ${ALL_IPS[@]}
    do
    echo "${allimageName} copy to ${all_ip}"
    ssh root@${all_ip} "ctr -n k8s.io images import k8simages/${allimageName}\.tar"
    done
  done
  
[root@master01 ~]# bash kubeadm/k8simage.sh                     #确认版本,提前下载镜像

提示:如上仅需Master01节点操作,从而实现所有节点镜像的分发。
注意相关版本,如上脚本为v1.30.3 Kubernetes版本所需镜像。

[root@master01 ~]# ctr -n k8s.io images ls        	#确认验证
[root@master02 ~]# crictl images ls
IMAGE                                                TAG                 IMAGE ID            SIZE
docker.io/calico/cni                                 v3.28.1             f6d76a1259a8c       94.6MB
docker.io/calico/kube-controllers                    v3.28.1             9d19dff735fa0       35MB
docker.io/calico/node                                v3.28.1             8bbeb9e1ee328       118MB
docker.io/longhornio/backing-image-manager           v1.6.2              9b8cf5184bda1       133MB
docker.io/longhornio/csi-attacher                    v4.5.1              ebcde6f69ddda       27.5MB
docker.io/longhornio/csi-node-driver-registrar       v2.9.2              438c692b0cb6d       10.8MB
docker.io/longhornio/csi-provisioner                 v3.6.4              cc753cf7b8127       28.7MB
docker.io/longhornio/csi-resizer                     v1.10.1             644d77abe33db       28.1MB
docker.io/longhornio/csi-snapshotter                 v6.3.4              eccecdceb86c0       26.9MB
docker.io/longhornio/livenessprobe                   v2.12.0             38ae1b6759b01       13.4MB
docker.io/longhornio/longhorn-engine                 v1.6.2              7fb50a1bbe317       142MB
docker.io/longhornio/longhorn-instance-manager       v1.6.2              23292e266e0eb       272MB
docker.io/longhornio/longhorn-manager                v1.6.2              6b0b2d18564be       112MB
docker.io/longhornio/longhorn-share-manager          v1.6.2              f578840264031       81.1MB
docker.io/longhornio/longhorn-ui                     v1.6.2              b1c8e3638fc43       75.6MB
docker.io/longhornio/support-bundle-kit              v0.0.37             df2168e6bf552       89.3MB
registry.k8s.io/coredns/coredns                      v1.11.1             cbb01a7bd410d       18.2MB
registry.k8s.io/etcd                                 3.5.12-0            3861cfcd7c04c       57.2MB
registry.k8s.io/ingress-nginx/controller             v1.11.1             5a3c471280784       105MB
registry.k8s.io/ingress-nginx/kube-webhook-certgen   v1.4.1              684c5ea3b61b2       23.9MB
registry.k8s.io/kube-apiserver                       v1.30.3             1f6d574d502f3       32.8MB
registry.k8s.io/kube-controller-manager              v1.30.3             76932a3b37d7e       31.1MB
registry.k8s.io/kube-proxy                           v1.30.3             55bb025d2cfa5       29MB
registry.k8s.io/kube-scheduler                       v1.30.3             3edc18e7b7672       19.3MB
registry.k8s.io/metrics-server/metrics-server        v0.7.1              a24c7c057ec87       19.5MB
registry.k8s.io/pause                                3.9                 e6f1816883972       319kB

Master01上初始化

Master01节点上执行初始化,即完成单节点的Kubernetes,其他节点采用添加的方式部署。

提示:kubeadm init过程会执行系统预检查,预检查通过则继续init,也可以提前执行如下命令进行预检查操作: kubeadm init phase preflight

[root@master01 ~]# kubeadm init --config=kubeadm/kubeadm-config.yaml --upload-certs                 #保留如下命令用于后续节点添加
[init] Using Kubernetes version: v1.30.3
[preflight] Running pre-flight checks
……
Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

  mkdir -p $HOME/.kube
  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
  sudo chown $(id -u):$(id -g) $HOME/.kube/config

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

  export KUBECONFIG=/etc/kubernetes/admin.conf

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
  https://kubernetes.io/docs/concepts/cluster-administration/addons/

You can now join any number of the control-plane node running the following command on each as root:

  kubeadm join 172.24.10.100:16443 --token 5ogx63.mjfb2mvyyebp30v6 \
	--discovery-token-ca-cert-hash sha256:35332dd14dac287b35b85af9fc03bd45af15d14248aa3c255dfc96abb1082021 \
	--control-plane --certificate-key 2a3eea130eb22d945cfee660c40a250731a1853e54bbf25ee13c0400d4a04ad1

Please note that the certificate-key gives access to cluster sensitive data, keep it secret!
As a safeguard, uploaded-certs will be deleted in two hours; If necessary, you can use
"kubeadm init phase upload-certs --upload-certs" to reload certs afterward.

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 172.24.10.100:16443 --token 5ogx63.mjfb2mvyyebp30v6 \
	--discovery-token-ca-cert-hash sha256:35332dd14dac287b35b85af9fc03bd45af15d14248aa3c255dfc96abb1082021

注意:如上token具有默认24小时的有效期,token和hash值可通过如下方式获取:
kubeadm token list
如果 Token 过期以后,可以输入以下命令,生成新的 Token:

kubeadm token create
openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -hex | sed 's/^.* //'

创建相关Kubernetes集群配置文件保存目录。

[root@master01 ~]# mkdir -p $HOME/.kube
[root@master01 ~]# sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
[root@master01 ~]# sudo chown $(id -u):$(id -g) $HOME/.kube/config
[root@master01 ~]# cat << EOF >> ~/.bashrc
export KUBECONFIG=$HOME/.kube/config
EOF                                                                                 #设置KUBECONFIG环境变量
[root@master01 ~]# echo "source <(kubectl completion bash)" >> ~/.bashrc
[root@master01 ~]# source ~/.bashrc

附加:初始化过程大致步骤如下:

  • [certs]:生成相关的各种证书
  • [control-plane]:创建Kubernetes控制节点的静态Pod
  • [etcd]:创建ETCD的静态Pod
  • [kubelet-start]:生成kubelet的配置文件”/var/lib/kubelet/config.yaml”
  • [kubeconfig]:生成相关的kubeconfig文件
  • [bootstraptoken]:生成token记录下来,后续使用kubeadm join往集群中添加节点时会用到
  • [addons]:附带的相关插件

提示:初始化仅需要在master01上执行,若初始化异常可通过 kubeadm reset -f kubeadm/kubeadm-config.yaml && rm -rf $HOME/.kube /etc/cni/ /etc/kubernetes/ 重置。

添加Master节点

采用 kubeadm join 将其他Master节点添加至集群。

[root@master02 ~]# kubeadm join 172.24.10.100:16443 --token 5ogx63.mjfb2mvyyebp30v6 \
	--discovery-token-ca-cert-hash sha256:35332dd14dac287b35b85af9fc03bd45af15d14248aa3c255dfc96abb1082021 \
	--control-plane --certificate-key 2a3eea130eb22d945cfee660c40a250731a1853e54bbf25ee13c0400d4a04ad1
[root@master02 ~]# mkdir -p $HOME/.kube
[root@master02 ~]# sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
[root@master02 ~]# sudo chown $(id -u):$(id -g) $HOME/.kube/config
[root@master02 ~]# cat << EOF >> ~/.bashrc
export KUBECONFIG=$HOME/.kube/config
EOF						               	                                            #设置KUBECONFIG环境变量
[root@master02 ~]# echo "source <(kubectl completion bash)" >> ~/.bashrc
[root@master02 ~]# source ~/.bashrc

提示:master03也如上操作,添加至当前集群的controlplane。
若添加异常可通过 kubeadm reset -f kubeadm/kubeadm-config.yaml && rm -rf $HOME/.kube /etc/cni/ /etc/kubernetes/ 重置。

安装NIC插件

NIC插件介绍

  • Calico 是一个安全的 L3 网络和网络策略提供者。
  • Canal 结合 Flannel 和 Calico, 提供网络和网络策略。
  • Cilium 是一个 L3 网络和网络策略插件, 能够透明的实施 HTTP/API/L7 策略。 同时支持路由(routing)和叠加/封装( overlay/encapsulation)模式。
  • Contiv 为多种用例提供可配置网络(使用 BGP 的原生 L3,使用 vxlan 的 overlay,经典 L2 和 Cisco-SDN/ACI)和丰富的策略框架。Contiv 项目完全开源。安装工具同时提供基于和不基于 kubeadm 的安装选项。
  • Flannel 是一个可以用于 Kubernetes 的 overlay 网络提供者。
    +Romana 是一个 pod 网络的层 3 解决方案,并且支持 NetworkPolicy API。Kubeadm add-on 安装细节可以在这里找到。
  • Weave Net 提供了在网络分组两端参与工作的网络和网络策略,并且不需要额外的数据库。
  • CNI-Genie 使 Kubernetes 无缝连接到一种 CNI 插件,例如:Flannel、Calico、Canal、Romana 或者 Weave。

提示:本方案使用Calico插件。

部署calico

确认相关配置,如MTU,网卡接口,Pod的IP地址段。
calico原文件可参考官方:
https://raw.githubusercontent.com/projectcalico/calico/v3.27.2/manifests/calico.yaml

[root@master01 ~]# vim kubeadm/calico/calico.yaml	    #检查配置
……
data:
……
  veth_mtu: "1450"
……
            - name: CALICO_IPV4POOL_CIDR
              value: "10.10.0.0/16"			            #配置Pod网段
……
            - name: IP_AUTODETECTION_METHOD
              value: "interface=eth.*"			        #检查节点之间的网卡
……
[root@master01 ~]# kubectl apply -f kubeadm/calico/calico.yaml
[root@master01 ~]# kubectl get pods --all-namespaces -o wide		#查看部署的所有Pod
NAMESPACE     NAME                                       READY   STATUS    RESTARTS   AGE     IP             NODE       NOMINATED NODE   READINESS GATES
kube-system   calico-kube-controllers-77d59654f4-ld47h   1/1     Running   0          63s     10.10.59.194   master02   <none>           <none>
kube-system   calico-node-5x2qt                          1/1     Running   0          63s     172.24.10.11   master01   <none>           <none>
kube-system   calico-node-hk9p6                          1/1     Running   0          63s     172.24.10.12   master02   <none>           <none>
kube-system   calico-node-ttbr5                          1/1     Running   0          63s     172.24.10.13   master03   <none>           <none>
kube-system   coredns-7db6d8ff4d-4swqk                   1/1     Running   0          8m      10.10.59.195   master02   <none>           <none>
kube-system   coredns-7db6d8ff4d-zv4n9                   1/1     Running   0          8m      10.10.59.193   master02   <none>           <none>
kube-system   etcd-master01                              1/1     Running   0          8m12s   172.24.10.11   master01   <none>           <none>
kube-system   etcd-master02                              1/1     Running   0          5m38s   172.24.10.12   master02   <none>           <none>
kube-system   etcd-master03                              1/1     Running   0          5m46s   172.24.10.13   master03   <none>           <none>
kube-system   kube-apiserver-master01                    1/1     Running   0          8m12s   172.24.10.11   master01   <none>           <none>
kube-system   kube-apiserver-master02                    1/1     Running   0          5m47s   172.24.10.12   master02   <none>           <none>
kube-system   kube-apiserver-master03                    1/1     Running   0          5m46s   172.24.10.13   master03   <none>           <none>
kube-system   kube-controller-manager-master01           1/1     Running   0          8m18s   172.24.10.11   master01   <none>           <none>
kube-system   kube-controller-manager-master02           1/1     Running   0          5m47s   172.24.10.12   master02   <none>           <none>
kube-system   kube-controller-manager-master03           1/1     Running   0          5m46s   172.24.10.13   master03   <none>           <none>
kube-system   kube-proxy-98dzr                           1/1     Running   0          8m      172.24.10.11   master01   <none>           <none>
kube-system   kube-proxy-wcgld                           1/1     Running   0          5m50s   172.24.10.13   master03   <none>           <none>
kube-system   kube-proxy-wf4tg                           1/1     Running   0          5m50s   172.24.10.12   master02   <none>           <none>
kube-system   kube-scheduler-master01                    1/1     Running   0          8m14s   172.24.10.11   master01   <none>           <none>
kube-system   kube-scheduler-master02                    1/1     Running   0          5m47s   172.24.10.12   master02   <none>           <none>
kube-system   kube-scheduler-master03                    1/1     Running   0          5m45s   172.24.10.13   master03   <none>           <none>

[root@master01 ~]# kubectl get nodes
NAME       STATUS   ROLES           AGE     VERSION
master01   Ready    control-plane   8m25s   v1.30.3
master02   Ready    control-plane   4m52s   v1.30.3
master03   Ready    control-plane   4m49s   v1.30.3

提示:官方calico参考: https://docs.projectcalico.org/manifests/calico.yaml

修改node端口范围

Kubernetes默认的端口范围为30000-32767,为便于后期大量的应用,如ingress的80、443端口,可开放全端口。
同时开放全端口范围后,使用的时候需要注意避开公共端口,如8080。

[root@master01 ~]# vi /etc/kubernetes/manifests/kube-apiserver.yaml                     #追加端口开放配置
……
    - --service-node-port-range=1-65535
……

提示:如上需要在所有Master节点操作。

添加Worker节点

添加Worker节点

[root@master01 ~]# source environment.sh
[root@master01 ~]# for node_ip in ${NODE_IPS[@]}
  do
    echo ">>> ${node_ip}"
    ssh root@${node_ip} "kubeadm join 172.24.10.100:16443 --token 5ogx63.mjfb2mvyyebp30v6 \
	--discovery-token-ca-cert-hash sha256:35332dd14dac287b35b85af9fc03bd45af15d14248aa3c255dfc96abb1082021"
    ssh root@${node_ip} "systemctl enable kubelet.service"
  done

提示:如上仅需Master01节点操作,从而实现所有Worker节点添加至集群,若添加异常可通过如下方式重置:

[root@worker01 ~]# kubeadm reset
[root@worker01 ~]# ifconfig kube-ipvs0 down
[root@worker01 ~]# ip link delete kube-ipvs0
[root@worker01 ~]# ifconfig tunl0@NONE down
[root@worker01 ~]# ip link delete tunl0@NONE
[root@worker01 ~]# rm -rf /var/lib/cni/

确认验证

[root@master01 ~]# kubectl get nodes			         	#节点状态
[root@master01 ~]# kubectl get cs			             	#组件状态
[root@master01 ~]# kubectl get serviceaccount		     	#服务账户
[root@master01 ~]# kubectl cluster-info			         	#集群信息
[root@master01 ~]# kubectl get pod -n kube-system -o wide	#所有服务状态

001
002

提示:更多Kubetcl使用参考: https://kubernetes.io/docs/reference/kubectl/kubectl/
https://kubernetes.io/docs/reference/kubectl/overview/
更多kubeadm使用参考: https://kubernetes.io/docs/reference/setup-tools/kubeadm/kubeadm/

Metrics部署

Metrics介绍

Kubernetes的早期版本依靠Heapster来实现完整的性能数据采集和监控功能,Kubernetes从1.8版本开始,性能数据开始以Metrics API的方式提供标准化接口,并且从1.10版本开始将Heapster替换为Metrics Server。在Kubernetes新的监控体系中,Metrics Server用于提供核心指标(Core Metrics),包括Node、Pod的CPU和内存使用指标,对其他自定义指标(Custom Metrics)的监控则由Prometheus等组件来完成。

Metrics Server是一个可扩展的、高效的容器资源度量,通常可用于Kubernetes内置的自动伸缩,即自动伸缩可依据metrics的度量指标。

Metrics Server从Kubelets收集资源指标,并通过Metrics API将它们暴露在Kubernetes apisserver中,供Pod水平或垂直自动伸缩使用。
kubectl top也可以访问Metrics API,可查看相关对象资源使用情况。

提示:当前官方建议Metrics Server仅用于自动伸缩,不要使用它来当做对Kubernetes的监控解决方案,或者监控解决方案的上游来源,对于完整的Kubernetes监控方案,可直接从Kubelet的/metrics/resource endpoint收集指标。

Metrics Server建议场景

使用Metrics Server的场景:
基于CPU/内存的水平自动缩放;
自动调整/建议容器所需的资源(了解有关垂直自动缩放的更多信息)

Metrics Server不建议场景

不建议使用Metrics Server的场景:

  • 非Kubernetes集群;
  • 集群资源对象资源消耗的准确依据;
  • 基于CPU/内存以外的其他资源的水平自动缩放。

Metrics特点

Metrics Server主要特点:

  • 在大多数集群上可以以单Pod工作;
  • 快速自动伸缩,且每15秒收集一次指标;
  • 资源消耗极低,在集群中每个节点上仅需1分片CPU和2 MB内存;
  • 可扩展支持最多5000个节点集群。

Metrics需求

Metrics Server对集群和网络配置有特定的需求依赖,这些需求依赖并不是所有集群默认开启的。
在使用Metrics Server之前,需要确保集群支持这些需求:

  • kube-apiserver必须启用聚合层(aggregation layer);
  • 节点必须启用Webhook身份验证和授权;
  • Kubelet证书需要由集群证书颁发机构签名(或者通过向Metrics Server传递--kubelet-insecure-tls禁用证书验证);
  • 容器运行时必须实现容器度量rpc(或有cAdvisor支持);
  • 网络应支持以下通信:
    • 控制平面到Metrics Server通信要求:控制平面节点需要到达Metrics Server的pod IP和端口10250(如果hostNetwork开启,则可以是自定义的node IP和对应的自定义端口,保持通信即可);
    • Metrics Server到所有节点的Kubelete通信要求:Metrics Server需要到达node节点地址和Kubelet端口。地址和端口在Kubelet中配置,并作为Node对象的一部分发布。.status.address和.status.daemonEndpoints.kubeletEndpoint.port定义地址和端口(默认10250)。Metrics Server将根据kubelet-preferred-address-types命令行标志提供的列表选择第一个节点地址(默认InternalIP,ExternalIP,Hostname)。

开启聚合层

有关聚合层知识参考:
https://blog.csdn.net/liukuan73/article/details/81352637
kubeadm方式部署默认已开启。

获取部署文件

根据实际生产环境,对Metrics Server的部署进行个性化修改,其他保持默认即可。
主要涉及:部署副本数为3,追加--kubelet-insecure-tls配置。

[root@master01 ~]# mkdir metrics
[root@master01 ~]# cd metrics/
[root@master01 metrics]# wget https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

[root@master01 metrics]# vi components.yaml
……
apiVersion: apps/v1
kind: Deployment
……
spec:
  replicas: 3						                                                                    	#根据集群规模调整副本数
    ……
    spec:
      hostNetwork: true                                                                                     #追加此行
      containers:
      - args:
        - --cert-dir=/tmp
        - --secure-port=10300                                                                               #修改端口                                                                               #修改端口
        - --kubelet-insecure-tls                                                                            #追加此行
        - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname,InternalDNS,ExternalDNS          #修改此args
        - --kubelet-use-node-status-port
        - --metric-resolution=15s
        image: registry.k8s.io/metrics-server/metrics-server:v0.7.1
        imagePullPolicy: IfNotPresent
    ……
        ports:
        - containerPort: 10300
    ……

提示:默认的10250会被kubelet当做服务监听的端口,因此建议修改端口。

正式部署

[root@master01 metrics]# kubectl apply -f components.yaml
[root@master01 metrics]# kubectl -n kube-system get pods -l k8s-app=metrics-server -o wide
NAME                              READY   STATUS    RESTARTS   AGE    IP             NODE       NOMINATED NODE   READINESS GATES
metrics-server-78bd46cc84-lm9r7   1/1     Running   0          42s    172.24.10.15   worker02   <none>           <none>
metrics-server-78bd46cc84-qsxtf   1/1     Running   0          112s   172.24.10.14   worker01   <none>           <none>
metrics-server-78bd46cc84-zjsn6   1/1     Running   0          78s    172.24.10.16   worker03   <none>           <none>

查看资源监控

可使用kubectl top查看相关监控项。

[root@master01 ~]# kubectl top nodes
[root@master01 ~]# kubectl top pods --all-namespaces

003

提示:Metrics Server提供的数据也可以供HPA控制器使用,以实现基于CPU使用率或内存使用值的Pod自动扩缩容功能。\
有关metrics更多部署参考:
https://kubernetes.io/docs/tasks/debug-application-cluster/resource-metrics-pipeline/
开启开启API Aggregation参考:
https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/apiserver-aggregation/
API Aggregation介绍参考:
https://kubernetes.io/docs/tasks/access-kubernetes-api/configure-aggregation-layer/

Nginx ingress部署

Kubernetes中的应用通常以Service对外暴露,而Service的表现形式为IP:Port,即工作在TCP/IP层。
对于基于HTTP的服务来说,不同的URL地址经常对应到不同的后端服务(RS)或者虚拟服务器(Virtual Host),这些应用层的转发机制仅通过Kubernetes的Service机制是无法实现的。

从Kubernetes 1.1版本开始新增Ingress资源对象,用于将不同URL的访问请求转发到后端不同的Service,以实现HTTP层的业务路由机制。
Kubernetes使用了一个Ingress策略规则和一个具体的Ingress Controller,两者结合实现了一个完整的Ingress负载均衡器。
使用Ingress进行负载分发时,Ingress Controller基于Ingress策略规则将客户端请求直接转发到Service对应的后端Endpoint(Pod)上,从而跳过kube-proxy的转发功能,kube-proxy不再起作用。

简单的理解就是:ingress使用DaemonSet或Deployment在相应Node上监听80或443,然后配合相应规则,因为Nginx外面绑定了宿主机80端口(就像 NodePort),本身又在集群内,那么向后直接转发到相应ServiceIP即可实现相应需求。
ingress controller + ingress 策略规则 ----> services。

同时当Ingress Controller提供的是对外服务,则实际上实现的是边缘路由器的功能。

典型的HTTP层路由的架构:

017

设置标签

建议对于非业务相关的应用,构建集群所需的应用(如Ingress),部署在master节点,从而复用master节点的高可用。
采用标签,结合部署的yaml中的tolerations,实现ingress部署在master节点的配置。

[root@master01 ~]# kubectl label nodes master0{1,2,3} ingress=enable

获取资源

获取部署所需的yaml资源。

[root@master01 ~]# mkdir ingress
[root@master01 ~]# cd ingress/
[root@master01 ingress]# wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.11.1/deploy/static/provider/baremetal/deploy.yaml

提示:ingress官方参考: https://github.com/kubernetes/ingress-nginx
https://kubernetes.github.io/ingress-nginx/deploy/

修改配置

为方便后续管理和排障,对相关Nginx ingress挂载时区,以便使Pod时间正确,从而相关记录日志能具有时效性。
同时对ingress做了简单配置,如日志格式等。

[root@master01 ingress]# vi deploy.yaml
    ……
---
apiVersion: v1
data:
  allow-snippet-annotations: "true"
  client-header-buffer-size: "512k"                                             #客户端请求头的缓冲区大小      
  large-client-header-buffers: "4 512k"                                         #设置用于读取大型客户端请求标头的最大值number和size缓冲区
  client-body-buffer-size: "128k"                                               #读取客户端请求body的缓冲区大小
  proxy-buffer-size: "256k"                                                     #代理缓冲区大小
  proxy-body-size: "50m"                                                        #代理body大小
  server-name-hash-bucket-size: "128"                                           #服务器名称哈希大小
  map-hash-bucket-size: "128"                                                   #map哈希大小
  ssl-ciphers: "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-R
SA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA
:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA"        #SSL加密套件
  ssl-protocols: "TLSv1 TLSv1.1 TLSv1.2"                                        #ssl 协议
  log-format-upstream: '{"time": "$time_iso8601", "remote_addr": "$proxy_protocol_addr", "x-forward-for": "$proxy_add_x_forwarded_for", "request_id": "$req_id","remote_user": "$remote_user", "bytes_sent": $bytes_sent, "request_time": $request_time, "sta
tus":$status, "vhost": "$host", "request_proto": "$server_protocol", "path": "$uri", "request_query": "$args", "request_length": $request_length, "duration": $request_time,"method": "$request_method", "http_referrer": "$http_referer", "http_user_agent":
 "$http_user_agent" }'                                                          #日志格式
kind: ConfigMap
……
---
apiVersion: v1
kind: Service
metadata:
……
spec:
  ipFamilies:
  - IPv4
  ipFamilyPolicy: SingleStack
  ports:
  - appProtocol: http
    name: http
    port: 80
    protocol: TCP 
    targetPort: http
    nodePort: 80                                                    #追加此行
  - appProtocol: https
    name: https
    port: 443
    protocol: TCP 
    targetPort: https
    nodePort: 443                                                   #追加此行
  selector:
    app.kubernetes.io/component: controller
    app.kubernetes.io/instance: ingress-nginx
    app.kubernetes.io/name: ingress-nginx
  type: NodePort
  externalTrafficPolicy: Local                                      #追加此行
……
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/component: controller
    app.kubernetes.io/instance: ingress-nginx
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
    app.kubernetes.io/version: 1.11.1
  name: ingress-nginx-controller
  namespace: ingress-nginx
spec:
  replicas: 3                                                       #配置副本数
……
    spec:
      containers:
      - args:
        - /nginx-ingress-controller
……
        image: registry.k8s.io/ingress-nginx/controller:v1.11.1     #修改image镜像
……
        volumeMounts:
……
        - mountPath: /etc/localtime                                 #挂载localtime
          name: timeconfig
          readOnly: true
      dnsPolicy: ClusterFirst
      nodeSelector:
        kubernetes.io/os: linux
        ingress: enable
      tolerations:
        - key: node-role.kubernetes.io/control-plane
          effect: NoSchedule                                        #追加nodeSelector和tolerations
……
      volumes:
      - name: webhook-cert
        secret:
          secretName: ingress-nginx-admission
      - name: timeconfig                                            #将hostpath配置为挂载卷
        hostPath:
          path: /etc/localtime
……
        image: registry.k8s.io/ingress-nginx/kube-webhook-certgen:v1.4.1      #修改image镜像
……
        image: registry.k8s.io/ingress-nginx/kube-webhook-certgen:v1.4.1      #修改image镜像
……

[root@master01 ingress]# kubectl apply -f deploy.yaml

提示:添加默认backend需要等待default-backend创建完成controllers才能成功部署,新版本ingress不再推荐添加default backend。

确认验证

查看Pod部署进度,是否成功完成。

[root@master01 ingress]# kubectl get pods -n ingress-nginx -o wide
[root@master01 ingress]# kubectl get svc -n ingress-nginx -o wide

004

提示:参考文档: https://github.com/kubernetes/ingress-nginx/blob/master/docs/deploy/index.md

Longhorn存储部署

Longhorn概述

Longhorn是用于Kubernetes的开源分布式块存储系统。
当前Kubernetes 1.30.3版本建议使用Longhorn 1.6.2 。

提示:更多介绍参考: https://github.com/longhorn/longhorn

基础软件安装

后续业务应用可能运行在任意节点位置,挂载操作需要在任何节点可正常执行。
所有节点均需要安装基础以来软件。

[root@master01 ~]# source environment.sh
[root@master01 ~]# for all_ip in ${ALL_IPS[@]}
  do
    echo ">>> ${all_ip}"
    ssh root@${all_ip} "yum -y install iscsi-initiator-utils &"
    ssh root@${all_ip} "systemctl enable iscsid --now"
  done

提示:所有节点都需要安装。

设置标签

在Master节点上部署存储组件的图形界面。

[root@master01 ~]# kubectl label nodes master0{1,2,3} longhorn-ui=enabled

提示:ui图形界面可复用master高可用,因此部署在master节点。

准备磁盘

Longhorn的分布式存储,建议独立磁盘设备专门作为存储卷,可提前挂载。
longhorn默认使用/var/lib/longhorn/作为设备路径,可提前挂载/dev/nvme0n2设备。
不同环境下裸磁盘的设备名不一样,根据实际环境为准。

[root@master01 ~]# source environment.sh

[root@master01 ~]# for node_ip in ${NODE_IPS[@]}
  do
    echo ">>> ${node_ip}"
    ssh root@${node_ip} "mkfs.xfs -f /dev/nvme0n2 && mkdir -p /var/lib/longhorn/ && echo '/dev/nvme0n2        /var/lib/longhorn        xfs        defaults        0 0' >> /etc/fstab && mount -a"
  done

配置Longhorn

根据实际生产环境,对Longhorn进行优化配置。

[root@master01 ~]# mkdir longhorn
[root@master01 ~]# cd longhorn/
[root@master01 longhorn]# wget https://raw.githubusercontent.com/longhorn/longhorn/v1.6.2/deploy/longhorn.yaml

[root@master01 longhorn]# vi longhorn.yaml
……
---
# Source: longhorn/templates/deployment-ui.yaml
apiVersion: apps/v1
kind: Deployment
……
spec:
  replicas: 2
……
      containers:
      - name: longhorn-ui
        image: longhornio/longhorn-ui:v1.6.0
……
      nodeSelector:
        longhorn-ui: enabled                                #追加标签选择
      tolerations:
        - key: node-role.kubernetes.io/control-plane        #添加容忍
          effect: NoSchedule
……

正式部署

基于优化的yaml进行部署。

[root@master01 ~]# cd  longhorn/
[root@master01 longhorn]# kubectl apply -f longhorn.yaml
[root@master01 longhorn]# kubectl -n longhorn-system get pods -o wide
NAME                                                READY   STATUS    RESTARTS        AGE     IP             NODE       NOMINATED NODE   READINESS GATES
csi-attacher-57689cc84b-55vw6                       1/1     Running   0               104s    10.10.19.82    worker03   <none>           <none>
csi-attacher-57689cc84b-gxm62                       1/1     Running   0               105s    10.10.30.82    worker02   <none>           <none>
csi-attacher-57689cc84b-jtqdw                       1/1     Running   0               104s    10.10.5.17     worker01   <none>           <none>
csi-provisioner-6c78dcb664-cswfz                    1/1     Running   0               104s    10.10.19.83    worker03   <none>           <none>
csi-provisioner-6c78dcb664-kdnhc                    1/1     Running   0               104s    10.10.5.18     worker01   <none>           <none>
csi-provisioner-6c78dcb664-vqs4z                    1/1     Running   0               104s    10.10.30.81    worker02   <none>           <none>
csi-resizer-7466f7b45f-8lbhp                        1/1     Running   0               104s    10.10.5.21     worker01   <none>           <none>
csi-resizer-7466f7b45f-9g7rw                        1/1     Running   0               104s    10.10.30.83    worker02   <none>           <none>
csi-resizer-7466f7b45f-xzsgs                        1/1     Running   0               104s    10.10.19.81    worker03   <none>           <none>
csi-snapshotter-58bf69fbd5-5b59k                    1/1     Running   0               104s    10.10.19.85    worker03   <none>           <none>
csi-snapshotter-58bf69fbd5-7q25t                    1/1     Running   0               104s    10.10.30.85    worker02   <none>           <none>
csi-snapshotter-58bf69fbd5-rprpq                    1/1     Running   0               104s    10.10.5.19     worker01   <none>           <none>
engine-image-ei-acb7590c-9b8wf                      1/1     Running   0               116s    10.10.30.79    worker02   <none>           <none>
engine-image-ei-acb7590c-bbcw9                      1/1     Running   0               116s    10.10.19.79    worker03   <none>           <none>
engine-image-ei-acb7590c-d4qlp                      1/1     Running   0               116s    10.10.5.15     worker01   <none>           <none>
instance-manager-0cf302d46e3eaf0be2c65de14febecb3   1/1     Running   0               110s    10.10.30.80    worker02   <none>           <none>
instance-manager-652604acb4423fc91cae625c664b813b   1/1     Running   0               2m21s   10.10.5.14     worker01   <none>           <none>
instance-manager-6e47bda67fc7278dee5cbb280e6a8fde   1/1     Running   0               110s    10.10.19.80    worker03   <none>           <none>
longhorn-csi-plugin-j92qz                           3/3     Running   0               104s    10.10.19.84    worker03   <none>           <none>
longhorn-csi-plugin-lsqzb                           3/3     Running   0               104s    10.10.5.20     worker01   <none>           <none>
longhorn-csi-plugin-nv7vk                           3/3     Running   0               104s    10.10.30.84    worker02   <none>           <none>
longhorn-driver-deployer-576d574c8-vw8hq            1/1     Running   0               4m53s   10.10.30.78    worker02   <none>           <none>
longhorn-manager-2vfpz                              1/1     Running   3 (2m16s ago)   4m53s   10.10.30.77    worker02   <none>           <none>
longhorn-manager-4d5w9                              1/1     Running   2 (2m19s ago)   4m53s   10.10.5.13     worker01   <none>           <none>
longhorn-manager-r55wx                              1/1     Running   3 (4m26s ago)   4m53s   10.10.19.78    worker03   <none>           <none>
longhorn-ui-7cfd57b47d-brh89                        1/1     Running   0               4m53s   10.10.59.198   master02   <none>           <none>
longhorn-ui-7cfd57b47d-qndks                        1/1     Running   0               4m53s   10.10.235.8    master03   <none>           <none>

提示:若部署异常可删除重建,若出现无法删除namespace,可通过如下操作进行删除:

wget https://raw.githubusercontent.com/longhorn/longhorn/v1.6.0/uninstall/uninstall.yaml
kubectl apply -f uninstall.yaml

kubectl get job/longhorn-uninstall -n longhorn-system -w

kubectl delete -f uninstall.yaml                                #等待任务完成再次执行delete

rm -rf /var/lib/longhorn/*

若依旧无法释放,参考《附098.Kubernetes故障排查记录》。

动态sc创建

部署Longhorn后,默认已创建一个名为longhorn的sc。

[root@master01 longhorn]# kubectl get sc
NAME                 PROVISIONER          RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
longhorn (default)   driver.longhorn.io   Delete          Immediate           true                   5m53s

测试PV及PVC

使用常见的Nginx Pod进行测试,模拟生产环境常见的Web类应用的持久性存储卷。


[root@master01 longhorn]# cat <<EOF > longhornpvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: longhorn-pvc
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: longhorn
  resources:
    requests:
      storage: 50Mi
EOF                                                                                 #创建PVC

[root@master01 longhorn]# cat <<EOF > longhornpod.yaml
---
apiVersion: v1
kind: Pod
metadata:
  name: longhorn-pod
  namespace: default
spec:
  containers:
  - name: volume-test
    image: nginx:stable-alpine
    imagePullPolicy: IfNotPresent
    volumeMounts:
    - name: volv
      mountPath: /usr/share/nginx/html
    ports:
    - containerPort: 80
  volumes:
  - name: volv
    persistentVolumeClaim:
      claimName: longhorn-pvc
EOF                                                                                 #创建Pod

[root@master01 longhorn]# kubectl apply -f longhornpvc.yaml

[root@master01 longhorn]# kubectl apply -f longhornpod.yaml

[root@master01 longhorn]# kubectl get pods
[root@master01 longhorn]# kubectl get pvc
[root@master01 longhorn]# kubectl get pv

008

Ingress暴露Longhorn

使用已部署完成的ingress将Longhorn UI暴露,以便于使用URL形式访问Longhorn图形界面进行Longhorn的基础管理。

[root@master01 longhorn]# yum -y install httpd-tools
[root@master01 longhorn]# htpasswd -c auth admin                                    #创建用户名和密码
New password: [输入密码]
Re-type new password: [输入密码]

提示:也可通过如下命令创建:
USER=admin; PASSWORD=admin1234; echo "${USER}:$(openssl passwd -stdin -apr1 <<< ${PASSWORD})" >> auth

[root@master01 longhorn]# kubectl -n longhorn-system create secret generic longhorn-basic-auth --from-file=auth
[root@master01 longhorn]# cat <<EOF > longhorn-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: longhorn-ingress
  namespace: longhorn-system
  annotations:
    nginx.ingress.kubernetes.io/auth-type: basic
    nginx.ingress.kubernetes.io/auth-secret: longhorn-basic-auth
    nginx.ingress.kubernetes.io/auth-realm: 'Authentication Required '
spec:
  ingressClassName: "nginx"
  rules:
  - host: longhorn.linuxsb.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: longhorn-frontend
            port: 
              number: 80
EOF

[root@master01 longhorn]# kubectl apply -f longhorn-ingress.yaml
[root@master01 longhorn]# kubectl -n longhorn-system get svc longhorn-frontend
[root@master01 longhorn]# kubectl -n longhorn-system get ingress longhorn-ingress
[root@master01 longhorn]# kubectl -n longhorn-system describe svc longhorn-frontend
[root@master01 longhorn]# kubectl -n longhorn-system describe ingress longhorn-ingress

005
006

确认验证

浏览器访问:longhorn.linuxsb.com ,并输入设置的账号和密码。

007

使用admin/[密码]登录查看。

008

Helm部署

helm介绍

Helm 是 Kubernetes 的软件包管理工具。包管理器类似 Ubuntu 中使用的apt、Centos中使用的yum 或者Python中的 pip 一样,能快速查找、下载和安装软件包。通常每个包称为一个Chart,一个Chart是一个目录(一般情况下会将目录进行打包压缩,形成name-version.tgz格式的单一文件,方便传输和存储)。
Helm 由客户端组件 helm 和服务端组件 Tiller 组成, 能够将一组K8S资源打包统一管理, 是查找、共享和使用为Kubernetes构建的软件的最佳方式。

Helm优势

在 Kubernetes中部署一个可以使用的应用,需要涉及到很多的 Kubernetes 资源的共同协作。
如安装一个 WordPress 博客,用到了一些 Kubernetes 的一些资源对象。包括 Deployment 用于部署应用、Service 提供服务发现、Secret 配置 WordPress 的用户名和密码,可能还需要 pv 和 pvc 来提供持久化服务。并且 WordPress 数据是存储在mariadb里面的,所以需要 mariadb 启动就绪后才能启动 WordPress。这些 k8s 资源过于分散,不方便进行管理。

基于如上场景,在 k8s 中部署一个应用,通常面临以下几个问题:
如何统一管理、配置和更新这些分散的 k8s 的应用资源文件;
如何分发和复用一套应用模板;
如何将应用的一系列资源当做一个软件包管理。

对于应用发布者而言,可以通过 Helm 打包应用、管理应用依赖关系、管理应用版本并发布应用到软件仓库。
对于使用者而言,使用 Helm 后不用需要编写复杂的应用部署文件,可以以简单的方式在 Kubernetes 上查找、安装、升级、回滚、卸载应用程序。

前置准备

Helm 将使用 kubectl 在已配置的集群上部署 Kubernetes 资源,因此需要如下前置准备:

  • 正在运行的 Kubernetes 集群;
  • 预配置的 kubectl 客户端和 Kubernetes 集群正确交互。

二进制安装Helm

建议采用二进制安装helm。

[root@master01 ~]# mkdir helm
[root@master01 ~]# cd helm/
[root@master01 helm]# wget https://repo.huaweicloud.com/helm/v3.15.3/helm-v3.15.3-linux-amd64.tar.gz
[root@master01 helm]# tar -zxvf helm-v3.15.3-linux-amd64.tar.gz
[root@master01 helm]# cp linux-amd64/helm /usr/local/bin/
[root@master01 helm]]# helm version		#查看安装版本
[root@master01 helm]]# echo 'source <(helm completion bash)' >> $HOME/.bashrc           #helm自动补全

提示:更多安装方式参考官方手册: https://helm.sh/docs/intro/install/

Helm操作

查找chart

helm search:可以用于搜索两种不同类型的源。
helm search hub:搜索 Helm Hub,该源包含来自许多不同仓库的Helm chart。
helm search repo:搜索已添加到本地头helm客户端(带有helm repo add)的仓库,该搜索是通过本地数据完成的,不需要连接公网。

[root@master01 ~]# helm search hub			                    #可搜索全部可用chart
[root@master01 ~]# helm search hub wordpress

添加repo

类似CentOS添加yum源,可以给helm仓库添加相关源。

[root@master01 ~]# helm repo list			                    #查看repo
[root@master01 ~]# helm repo add stable http://mirror.azure.cn/kubernetes/charts
[root@master01 ~]# helm repo add aliyun https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts
[root@master01 ~]# helm repo add jetstack https://charts.jetstack.io

[root@master01 ~]# helm search repo stable
[root@master01 ~]# helm search repo aliyun		                #搜索repo中的chart
[root@master01 ~]# helm repo update			                    #更新repo的chart

Dashboard部署

dashboard介绍

dashboard是基于Web的Kubernetes用户界面,即WebUI。
可以使用dashboard将容器化应用程序部署到Kubernetes集群,对容器化应用程序进行故障排除,以及管理集群资源。
可以使用dashboard来查看群集上运行的应用程序,以及创建或修改单个Kubernetes资源(例如部署、任务、守护进程等)。
可以使用部署向导扩展部署,启动滚动更新,重新启动Pod或部署新应用程序。
dashboard还提供有关群集中Kubernetes资源状态以及可能发生的任何错误的信息。
通常生产环境中建议部署dashboard,以便于图形化来完成基础运维。

从7.0.0版本开始,社区已放弃了对基于manifest安装的支持,现在只支持基于helm的安装。 由于多容器设置和对Kong网关API代理的严重依赖,原有基于yaml清单安装的方式不可行的。

同时基于helm的安装,部署速度更快,并且可以更好地控制Dashboard运行所需的所有依赖项。并且已经改变了版本控制方案,并从Helm chart中删除了appVersion。
因为,使用多容器设置,每个模块现在都是单独的版本,Helm chart版本现在可以被视为应用版本。

设置标签

基于最佳实践,非业务应用,或集群自身的应用都部署在Master节点。

[root@master01 ~]# kubectl label nodes master0{1,2,3} dashboard=enable

提示:建议对于Kubernetes自身相关的应用(如dashboard),此类非业务应用部署在master节点。

创建证书

默认dashboard会自动创建证书,同时使用对应证书创建secret。生产环境可以启用相应的域名进行部署dashboard,因此需要将对于的域名制作为TLS证书。
本实验已获取免费一年的证书,免费证书获取可参考:
https://freessl.cn.
将已获取的证书上传至对应目录。

[root@master01 ~]# mkdir -p /root/dashboard/certs
[root@master01 ~]# cd /root/dashboard/certs
[root@master01 certs]# mv web.linuxsb.com.crt tls.crt
[root@master01 certs]# mv web.linuxsb.com.key tls.key
[root@master01 certs]# ll
total 8.0K
-rw-r--r-- 1 root root 3.9K Aug  8 16:15 tls.crt
-rw-r--r-- 1 root root 1.7K Aug  8 16:15 tls.key

提示:也可手动如下操作创建自签证书:

[root@master01 ~]# openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj "/C=CN/ST=ZheJiang/L=HangZhou/O=Xianghy/OU=Xianghy/CN=webui.linuxsb.com"

手动创建secret

自定义证书的场景,建议提前使用对应的证书创建secret。

[root@master01 ~]# kubectl create ns kubernetes-dashboard	                                        #v3版本dashboard独立ns
[root@master01 ~]# kubectl create secret generic kubernetes-dashboard-certs --from-file=/root/dashboard/certs/ -n kubernetes-dashboard
[root@master01 ~]# kubectl get secret kubernetes-dashboard-certs -n kubernetes-dashboard -o yaml	#查看证书信息

添加repo

添加kubernetes-dashboard的repo仓库。

[root@master01 ~]# helm repo add kubernetes-dashboard https://kubernetes.github.io/dashboard/

[root@master01 ~]# helm repo list
NAME                	URL                                     
……           
kubernetes-dashboard	https://kubernetes.github.io/dashboard/

编辑配置

根据实际情况修改默认的chart values,未配置的项表示使用默认值。

如下yaml主要做了几项自定义配置:

  • 指定dashboard部署在master节点,将其归属为集群自有应用,而非业务应用;
  • 指定了使用自有的TLS证书,及https的ingress域名;
  • 指定了污点能接受master节点;
  • 指定了Pod挂载本地时间文件,使Pod时钟正确。

kubernetes-dashboard默认的values值参考
kubernetes-dashboard values

[root@master01 ~]# cd /root/dashboard/
[root@master01 dashboard]# vi myvalues.yaml

app:
  mode: 'dashboard'
  scheduling:
    nodeSelector: {"dashboard": "enable"}
  ingress:
    enabled: true
    hosts:
      # - localhost
      - web.linuxsb.com
    ingressClassName: nginx
    useDefaultIngressClass: false
    annotations: 
      nginx.ingress.kubernetes.io/ssl-redirect: "true"
    tls:
      enabled: true
      secretName: "kubernetes-dashboard-certs"
  tolerations:
    - key: node-role.kubernetes.io/control-plane
      effect: NoSchedule

auth:
  nodeSelector: {"dashboard": "enable"}

# API deployment configuration
api:
  scaling:
    replicas: 3
  containers:
    volumeMounts:
      - mountPath: /tmp
        name: tmp-volume
      - mountPath: /etc/localtime
        name: timeconfig
  volumes:
    - name: tmp-volume
      emptyDir: {}
    - name: timeconfig
      hostPath:
        path: /etc/localtime
  nodeSelector: {"dashboard": "enable"}

# WEB UI deployment configuration
web:
  role: web
  scaling:
    replicas: 3
    revisionHistoryLimit: 10
  containers:
    volumeMounts:
      - mountPath: /tmp
        name: tmp-volume
      - mountPath: /etc/localtime
        name: timeconfig
  volumes:
    - name: tmp-volume
      emptyDir: {}
    - name: timeconfig
      hostPath:
        path: /etc/localtime
  nodeSelector: {"dashboard": "enable"}

# Metrics Scraper
metricsScraper:
  scaling:
    replicas: 3
    revisionHistoryLimit: 10
  containers:
    volumeMounts:
      - mountPath: /tmp
        name: tmp-volume
      - mountPath: /etc/localtime
        name: timeconfig
  volumes:
    - name: tmp-volume
      emptyDir: {}
    - name: timeconfig
      hostPath:
        path: /etc/localtime
  nodeSelector: {"dashboard": "enable"}

kong:
  nodeSelector: {"dashboard": "enable"}

正式部署

根据生产环境最佳实践进行调优,调优完成后开始部署。

[root@master01 dashboard]# helm upgrade --install kubernetes-dashboard kubernetes-dashboard/kubernetes-dashboard --create-namespace --namespace kubernetes-dashboard -f myvalues.yaml

[root@master01 dashboard]# helm -n kubernetes-dashboard list
NAME                	NAMESPACE           	REVISION	UPDATED                                	STATUS  	CHART                     	APP VERSION
kubernetes-dashboard	kubernetes-dashboard	1       	2024-08-14 19:39:29.034973262 +0800 CST	deployed	kubernetes-dashboard-7.5.0

[root@master01 dashboard]# kubectl -n kubernetes-dashboard get all
[root@master01 dashboard]# kubectl -n kubernetes-dashboard get deployments.apps
[root@master01 dashboard]# kubectl -n kubernetes-dashboard get services
[root@master01 dashboard]# kubectl -n kubernetes-dashboard get pods -o wide
[root@master01 dashboard]# kubectl -n kubernetes-dashboard get svc
[root@master01 dashboard]# kubectl -n kubernetes-dashboard get ingress -o wide

009
010
011

创建管理员账户

建议创建管理员账户,dashboard默认没有创建具有管理员权限的账户,同时v7版本登录只支持token方式。
因此建议创建管理员权限的用户,然后创建此用户的token,然后使用此token进行登录。

[root@master01 dashboard]# cat <<EOF > dashboard-admin.yaml
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: admin
  namespace: kubernetes-dashboard

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: admin
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: admin
  namespace: kubernetes-dashboard
  
---
apiVersion: v1
kind: Secret
type: kubernetes.io/service-account-token
metadata:
  name: admin
  namespace: kubernetes-dashboard
  annotations:
    kubernetes.io/service-account.name: "admin"
EOF

[root@master01 dashboard]# kubectl apply -f dashboard-admin.yaml

查看token

使用token相对复杂,可将token添加至kubeconfig文件中,使用KubeConfig文件访问dashboard。

[root@master01 dashboard]# ADMIN_SECRET=$(kubectl -n kubernetes-dashboard get secret | grep admin | awk '{print $1}')
[root@master01 dashboard]# DASHBOARD_LOGIN_TOKEN=$(kubectl describe secret -n kubernetes-dashboard ${ADMIN_SECRET} | grep -E '^token' | awk '{print $2}')
[root@master01 dashboard]# echo ${DASHBOARD_LOGIN_TOKEN}

提示:也可通过如下方式获取name为admin的secret的token。
kubectl -n kubernetes-dashboard get secret admin -o jsonpath={".data.token"} | base64 -d

将web.linuxsb.com.crt证书文件导入,以便于浏览器使用该文件登录。

导入证书

将webui.linuxsb.com.crt证书导入浏览器,并设置为信任,可规避证书不受信任的弹出。

测试访问dashboard

本实验采用ingress所暴露的域名:
https://web.linuxsb.com
使用对应admin用户的token进行访问。

012

登录后默认进入的是default命名空间,可切换至其他对应的namespace,对整个Kubernetes进行管理和查看。

013

提示:更多dashboard访问方式及认证可参考 附004.Kubernetes Dashboard简介及使用
dashboard登录整个流程可参考: https://www.cnadn.net/post/2613.html

扩展:集群扩容及缩容

集群扩容

  • master节点扩容
    参考:添加Master节点 步骤
  • worker节点扩容
    参考:添加Worker节点 步骤

集群缩容

  • master节点缩容
    Master节点缩容的时候会自动将Pod迁移至其他节点。
[root@master01 ~]# kubectl drain master03 --delete-emptydir-data --force --ignore-daemonsets
[root@master01 ~]# kubectl delete node master03
[root@master03 ~]# kubeadm reset -f && rm -rf $HOME/.kube
  • worker节点缩容
    Worker节点缩容的时候会自动将Pod迁移至其他节点。
[root@master01 ~]# kubectl drain worker04 --delete-emptydir-data --force --ignore-daemonsets
[root@master01 ~]# kubectl delete node worker04
[root@worker04 ~]# kubeadm reset -f
[root@worker04 ~]# rm -rf /etc/kubernetes/admin.conf /etc/kubernetes/kubelet.conf /etc/kubernetes/bootstrap-kubelet.conf /etc/kubernetes/controller-manager.conf /etc/kubernetes/scheduler.conf

大家好,我是
晓凡

一、日志概念

日志的重要性不用我多说了,日志,简单来说就是记录。

用来记录程序运行时发生的事情。比如,程序启动了、执行了某个操作、遇到了问题等等,这些都可以通过日志记录下来。

想象一下,你开了一家店,每天的营业额、顾客的反馈、商品的进出、库存等等,你都会记录下来。这就像是程序的日志。比如:

  • 电商网站
    :记录用户的登录、浏览、购买行为,监控交易过程,及时发现异常交易;通过日志分析你的浏览记录,实现精准推送等等
  • 服务器
    :记录服务器的启动、运行、关闭状态,以及发生的各种错误,帮助管理员及时发现并解决问题。

1.1 日志的作用

  1. 调试帮助
    :当程序出现问题时,通过查看日志,可以快速定位问题发生的地方和原因。
  2. 监控运行状态
    :通过日志可以了解程序的运行状态,比如用户的操作、系统的性能等。
  3. 安全审计
    :在需要记录用户行为或系统操作的场合,日志可以作为审计的依据。

1.2 具体示例

public class SimpleApp {
    public static void main(String[] args) {
        System.out.println("程序启动");

        // 假设这里是用户输入数据
        String userInput = "Hello, World!";
        System.out.println("用户输入了: " + userInput);

        // 处理数据
        String result = processInput(userInput);
        System.out.println("处理结果: " + result);
        try {
            //可能异常的逻辑代码
        }catch(Exception e){
            e.printStackTrace()
        }

        // 程序结束
        System.out.println("程序结束");
    }

    private static String processInput(String input) {
        // 这里是处理逻辑
        return "Processed: " + input;
    }
}

上面的代码我们不陌生了吧,我们使用
System.out.println
来打印程序的运行状态,使用
e.printStackTrace()
来打印信息和错误

这就是没有日志框架时,最简单直接的日志打印方式

这种方式简单直接,但也有一些缺点:

  • 灵活性差
    :不能方便地控制日志的输出格式、级别等。
  • 性能问题
    :大量日志输出可能会影响程序性能。
  • 不易管理
    :日志信息混在标准输出中,不易于查找和分析。

所以我们要引入各种功能强大的日志框架进行日志管理

二、主流日志框架

日志框架由日志门面和日志实现构成,具体如下图所示

主流日志框架

2.1 日志门面

顾名思义,日志门面,就像是一个团队的领导者一样,只负责制定规则,安排任务,而具体干活的则交给苦逼的打工人(日志具体实现)即可。

日志门面提供了一套标准的日志记录接口,而具体的日志记录工作则由不同的日志框架来完成。

这样做的好处是,可以在不修改代码的情况下,通过配置来切换不同的日志框架。

正如职场中,一个打工人跑路了,在不需要太多成本,不用做太多改变的情况下,新招一个更便宜的打工人也可完成同样的任务实现快速切换,好像有点扯远了

主流的日志门面框架主要有:

  • SLF4J
    :这是一个非常流行的日志门面,它提供了一套简单的日志记录接口,并且可以与多种日志框架(如Log4j、Logback等)配合使用。
  • JCL
    :这是早期的一个日志门面

2.2 日志实现

通过是实现日志门面接口来完成日志记录,实实在在的打工人无疑了

主流的日志实现框架有:

  • JUL

    Java
    自带的日志框架 ,功能相对基础,性能一般,但对于简单的日志需求来说足够用了。

  • Log4j

​ 个非常老牌的日志框架,功能非常强大,可以自定义很多日志的细节,比如日志级别、输出格式、输出目的地等。现由Apache软件基金会维护

  • Log4j2

​ 也是Apache软件基金会开发,相比
Log4j

Log4j2
在性能上有显著提升,同时保持了丰富的功能,支持异步日志处理,适合高性能需求的场景

  • Logback

​ 由
Log4j
的原开发者之一主导开发,
Spring Boot
默认日志,轻量级,性能优秀,功能也比较全面

三、JUL日志框架

3.1 主要组件

  1. Logger
    :日志记录器,是日志系统的核心,用来生成日志记录。
  2. Handler
    :日志处理器,负责将日志信息输出到不同的目的地,比如控制台、文件等。可以为每个Logger配置一个或多个
    Handler
  3. Formatter
    :日志格式化器,负责定义日志的输出格式。比如时间戳、日志级别、消息等。
  4. Level
    :设置日志级别,常见的级别有
    SEVERE

    WARNING

    INFO

    CONFIG

    FINE

    FINER

    FINEST
    等。
  5. Filter
    : 这个组件用来过滤日志记录。你可以设置一些规则,只有满足这些规则的日志才会被记录。
  6. Log Record:
    这是日志记录本身,包含了日志的所有信息,比如时间、日志级别、消息等

3.2 使用步骤

  1. 获取
    Logger
    实例。
  2. 添加
    Handler
  3. 为上一步添加的
    Handler
    设置日志级别(
    Level
    )和格式输出(
    Formatter
  4. 创建
    Filter
    过滤器

  5. Logger
    实例添加日志处理器(
    Handler
    )和日志过滤器(
    Filter
  6. 记录日志。

jul使用步骤

3.3 入门案例

public class LogQuickTest {
    @Test
    public void testLogQuick(){
        //创建日志记录对象
        Logger logger = Logger.getLogger("com.xiezhr");
        //日志记录输出
        logger.info("这是一个info日志");
        logger.log(Level.INFO,"这是一个info日志");

        String name="程序员晓凡";
        Integer age=18;
        logger.log(Level.INFO,"姓名:{0},年龄:{1}",new Object[]{name,age});

    }
}

JUT入门案例

3.4 日志级别

日志级别系统,用来区分日志的重要性

3.4.1 日志级别
  1. SEVERE
    (严重):这是最高级别的日志,用来记录严重错误,比如系统崩溃、数据丢失等。这类日志通常需要立即关注和处理。
  2. WARNING
    (警告):用来记录可能不会立即影响系统运行,但可能表明潜在问题的信息。比如,某个操作没有达到预期效果,或者系统资源接近耗尽。
  3. INFO
    (信息):用来记录一般性的信息,比如程序运行的状态、重要的操作步骤等。这类信息对于了解程序的运行情况很有帮助,但通常不需要立即处理。
  4. CONFIG
    (配置):用来记录配置信息,比如程序启动时加载的配置文件、初始化的参数等。这类日志有助于调试和验证程序的配置是否正确。
  5. FINE
    (详细):用来记录更详细的信息,比如程序内部的执行细节、变量的值等。这类日志对于开发者在调试程序时了解程序的内部状态非常有用。
  6. FINER
    (更详细):比FINE级别更细的日志,记录更深入的执行细节。通常用于深入分析程序的运行情况。
  7. FINEST
    (最详细):这是最低级别的日志,记录最详细的信息,包括程序的每一步执行细节。这类日志可能会产生大量的输出,通常只在需要非常详细的调试信息时使用。
3.4.2 级别关系

SEVERE
>
WARNING
>
INFO
>
CONFIG
>
FINE
>
FINER
>
FINEST

日志级别越高,记录的信息越重要。当你设置一个日志级别时,比如INFO,那么INFO级别以及以上的日志(SEVERE和WARNING)都会被记录,而FINE、FINER和FINEST级别的日志则会被忽略

3.5 详细使用案例(硬编码)

这里我们按照上面的步骤创建一个日志记录器,将日志文件分别输出到控制台和文件中

public class LoggingExampleTest {

    @Test
    public void testLogging() {
        // 获取日志记录器
        Logger logger = Logger.getLogger("LoggingExample");

        // 设置日志级别为INFO,这意味着INFO级别及以上的日志会被记录
        logger.setLevel(Level.INFO);

        // 创建控制台Handler 将日志输出到控制台
        // 并设置其日志级别和Formatter
        ConsoleHandler consoleHandler = new ConsoleHandler();
        consoleHandler.setLevel(Level.WARNING); // 控制台只输出WARNING及以上级别的日志
        consoleHandler.setFormatter(new SimpleFormatter() {
            @Override
            public synchronized String format(LogRecord record) {
                // 自定义日志格式
                return String.format("%1$tF %1$tT [%2$s] %3$s %n", record.getMillis(), record.getLevel(), record.getMessage());
            }
        });
        logger.addHandler(consoleHandler);

        // 创建文件Handler 将日志输出到文件
        // 并设置其日志级别和Formatter
        try {
            FileHandler fileHandler = new FileHandler("app.log", true);
            fileHandler.setLevel(Level.ALL); // 文件将记录所有级别的日志
            fileHandler.setFormatter(new SimpleFormatter() {
                @Override
                public synchronized String format(LogRecord record) {
                    // 自定义日志格式
                    return String.format("%1$tF %1$tT [%2$s] %3$s %n", record.getMillis(), record.getLevel(), record.getMessage());
                }
            });
            logger.addHandler(fileHandler);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 创建并设置Filter
        Filter filter = new Filter() {
            @Override
            public boolean isLoggable(LogRecord record) {
                // 这里可以添加过滤逻辑,例如只记录包含特定字符串的日志
                return record.getMessage().contains("important");
            }
        };

        // 将Filter应用到Logger
        //logger.setFilter(filter);

        // 记录不同级别的日志
        logger.severe("严重错误信息 - 应记录到控制台和文件");
        logger.warning("警告信息 - 应记录到控制台和文件");
        logger.info("常规信息 - 只记录到文件");
        logger.config("配置信息 - 只记录到文件");
        logger.fine("详细日志 - 只记录到文件");


        // 这条日志将被Filter过滤掉,不会记录
        logger.info("这条信息不重要,将被过滤");

        // 这条日志将被记录,因为消息中包含"important"
        logger.info("这条信息很重要,将被记录到控制台和文件");
    }
}       

① 控制台日志输出

1、控制台输出结果

②日志文件输出
app.log
内容

2、文件中输出日志

代码解释

  1. Logger获取
    :首先获取一个名为
    LoggingExample

    Logger
    实例。
  2. 设置日志级别
    :将Logger的日志级别设置为
    INFO
    ,这意味着INFO及以上级别的日志将被记录。
  3. 控制台Handler
    :创建一个
    ConsoleHandler
    实例,设置其日志级别为
    WARNING
    ,并且自定义了日志的输出格式。
  4. 文件Handler
    :尝试创建一个
    FileHandler
    实例,将日志写入到
    app.log
    文件中,并设置其日志级别为
    ALL
    ,意味着所有级别的日志都将被记录到文件。
  5. 自定义Formatter
    :为Handler创建自定义的
    SimpleFormatter
    ,用于定义日志的输出格式。
  6. Filter设置
    :创建一个实现了
    Filter
    接口的匿名内部类,并重写
    isLoggable
    方法,实现过滤逻辑,这里只记录消息中包含"important"字符串的日志。
  7. 应用Filter
    :将创建的Filter应用到Logger上。
  8. 记录日志
    :记录不同级别的日志,展示不同级别的日志如何被Handler和Filter处理。
  9. 日志记录
    :一些日志将根据设置的日志级别、Handler和Filter的规则被记录到控制台或文件,或者被忽略。

3.6 日志配置文件

以上3.4小节通过硬编码的方式打印输出日志,这样的方式很不利于后期的管理与维护,这小节我们将使用配置文件的方式进行日志输出

① 在resources下面新建
logconfig.properties
文件,内容如下

# 指定日志处理器为:ConsoleHandler,FileHandler 表示同时使用控制台和文件处理器
handlers= java.util.logging.ConsoleHandler,java.util.logging.FileHandler

#设置默认的日志级别为:ALL
.level= ALL

# 配置自定义 Logger
com.xiezhr.handlers = com.xiezhr.DefConsoleHandler
com.xiezhr.level = CONFIG

# 如果想要使用自定义配置,需要关闭默认配置
com.xiezhr.useParentHanlders =true

# 向日志文件输出的 handler 对象
# 指定日志文件路径 当文件数为1时 日志为/logs/java0.log
java.util.logging.FileHandler.pattern = /logs/java%u.log
# 指定日志文件内容大小,下面配置表示日志文件达到 50000 字节时,自动创建新的日志文件
java.util.logging.FileHandler.limit = 50000
# 指定日志文件数量,下面配置表示只保留 1 个日志文件
java.util.logging.FileHandler.count = 1
# 指定 handler 对象日志消息格式对象
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
# 指定 handler 对象的字符集为 UTF-8 ,防止出现乱码
java.util.logging.FileHandler.encoding = UTF-8
# 指定向文件中写入日志消息时,是否追加到文件末尾,true 表示追加,false 表示覆盖
java.util.logging.FileHandler.append = true


# 向控制台输出的 handler 对象
# 指定 handler 对象的日志级别
java.util.logging.ConsoleHandler.level =WARNING
# 指定 handler 对象的日志消息格式对象
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
# 指定 handler 对象的字符集
java.util.logging.ConsoleHandler.encoding = UTF-8

# 指定日志消息格式
java.util.logging.SimpleFormatter.format = [%1$tF %1$tT] %4$s: %5$s %n

注意:
设置日志消息格式中(后面一小节会详细讲解)

  • %1$tF
    :这个占位符表示日志记录的时间,格式为
    yyyy-MM-dd
    ,其中
    1$
    表示这是第一个参数
    tF
    是日期的格式化代码
  • %1$tT
    :这个占位符表示日志记录的时间,格式为
    HH:mm:ss.SSS
    ,即小时:分钟:秒.毫秒
    1$
    表示这是第一个参数,
    tT
    是时间的格式化代码
  • %4$s
    : 表示日志级别,
    level =WARNING
    输出警告 level =INFO 输出消息
  • %5$s
    : 表示日志消息
  • %n
    :这个占位符表示换行符,每条日志记录之后会有一个换行,以便在查看日志时能够清晰地区分每条记录。

② 日志测试

@Test
public void testLogProperties()throws Exception{

    // 1、读取配置文件,通过类加载器
    InputStream ins = LoggingExampleTest.class.getClassLoader().getResourceAsStream("logconfig.properties");
    // 2、创建LogManager
    LogManager logManager = LogManager.getLogManager();
    // 3、通过LogManager加载配置文件
    logManager.readConfiguration(ins);

    // 4、创建日志记录器
    Logger logger = Logger.getLogger("com.xiezhr");

    // 5、记录不同级别的日志
    logger.severe("这是一条severe级别信息");
    logger.warning("这是一条warning级别信息");


}

执行上面代码后

控制台输出

控制台输出

java0.log文件输出:

ava0.log文件输出

3.7 日志格式化

上面两个小节中,不管是通过编码或者配置文件 都对日志进行了格式化

① 编码设置日志格式

fileHandler.setFormatter(new SimpleFormatter() {
    @Override
    public synchronized String format(LogRecord record) {
        // 自定义日志格式
        return String.format("%1$tF %1$tT [%2$s] %3$s %n", record.getMillis(), record.getLevel(), record.getMessage());
    }
});

② 配置文件指定日志格式

# 指定日志消息格式
java.util.logging.SimpleFormatter.format = [%1$tF %1$tT] %4$s: %5$s %n

上面设置的日志格式设置你看懂了么?

不管是哪种方式设置日志格式,我们看源码最终都是通过
String.format
函数来实现的,所有我们有必要学一学
String
类提供的
format
这个方法的使用

new SimpleFormatter

3.7.1
String

format
方法

String

format
方法用来格式化字符串。

format
方法就像是一个模板,你可以在这个模板里插入你想要的数据,然后它就会帮你生成一个格式化好的字符串。

我们先来看看下面这个简单例子

@Test
public void testStringFormatter()throws Exception{
    String name = "晓凡";
    Integer age = 18;

    // 使用String.format()方法格式化字符串
    String xiaofan = String.format("%s今年%d岁", name, age);
    System.out.println(xiaofan);
}
//输出
晓凡今年18岁
3.7.2 常用占位符

%s

%d
为占位符,不同类型需要不同占位符,那么还有哪些常用转换符呢?

占位符 详细说明 示例
%s 字符串类型**** “喜欢晓凡请关注”
%c 字符类型 ‘x’
%b 布尔类型 true
%d 整数类型(十进制) 666
%x 整数类型(十六进制) FF
%o 整数类型(八进制) 77
%f 浮点类型 8.88
%a 十六进制浮点类型 FF.34
%e 指数类型 1.28e+5
%n 换行符
%tx 日期和时间类型(x代表不同的日期与时间转换符)
3.7.3 特殊符号搭配使用
符号 说明 示例 结果
0 指定数字、字符前面补0,用于对齐 ("%04d",6) 0006
空格 指定数字、字符前面补空格,用于对齐 ("[% 4s]",x) [ x]
以“,”对数字分组显示(常用于金额) ("%,f,666666.66") 666,666.6600

注意:
默认情况下,可变参数是按照顺序依次替换,但是我们可以通过“数字$”来重复利用可变参数

@Test
public void testStringFormatter()throws Exception{
    String name = "晓凡";
    Integer age = 18;

    // 使用String.format()方法格式化字符串
    String xiaofan = String.format("%s今年%d岁", name, age);
    System.out.println(xiaofan);
    //
    String xiaofan1 = String.format("%s今年%d岁,%1$s的公众号是:程序员晓凡", name, age);
    System.out.println(xiaofan1);
}
//输出
晓凡今年18岁
晓凡今年18岁,晓凡的公众号是:程序员晓凡

上面例子中我们通过
%1$s
重复使用第一个参数
name

3.7.4 日期格式化

上面我们说到%tx,x代表日期转换符,其具体含义如下

符号 描述 示例
c 包含全部日期和时间信息 周六 8月 03 17:16:37 CST 2024
F "年-月-日" 格式 2024-08-03
D "月/日/年"格式 08/03/24
d 03
r
HH:MM:SS PM
”格式(12小时制)
05:16:37 下午
R
HH:MM
”格式(24小时制)
17:16
T
HH:MM:SS
”格式(24小时制)
17:16:37
b 月份本地化 8月
y 两位年 24
Y 四位年 2024
m 08
H 时(24小时制) 17
I 时(12小时制) 05
M 16
S 37
s 秒为单位的时间戳 1722677530
p 上午还是下午 下午

四、Log4j日志框架

Log4j 是Apache软件基金组织旗下的一款开源日志框架,是一款比较老的日志框架,目前已出log4j2,它在log4j上做了很大改动,性能提升了不少。但是有些老项目还会在使用,所以我们也来说一说

官网:
https://logging.apache.org/log4j/1.x/

注意:
从官网,我们可以看到项目管理委员会宣布
Log4j 1. x
已终止使用。建议用户升级到
Log4j 2

log4j官网

4.1 快速入门

4.1.1 添加依赖
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>
<!--为了方便测试,我们引入junit-->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>test</scope>
</dependency>
4.1.2 log4j入门代码
@Test
public void testLog4jQuick(){

    //初始化日志配置信息,不需要配置文件
    BasicConfigurator.configure();
    //获取日志记录器
    Logger logger = Logger.getLogger(Log4jTest.class);
    //通过各种日志级别打印日志
    logger.fatal("这是一条致命的信息");  // 严重错误,一般会造成系统崩溃
    logger.error("这是一条错误的信息");  // 出现错误时,比如出错了但是不影响系统继续运行
    logger.warn("这是一条警告的信息");   // 警告级别,比如要告警的时候
    logger.info("这是一条普通的信息");  // 一般信息,比如记录普通的方法执行
    logger.debug("这是一条调试的信息"); // 调试信息,比如调试的时候打印的信息
    logger.trace("这是一条追踪的信息");  // 追踪信息,比如追踪程序运行路径
}
//输出
0 [main] FATAL Log4jTest  - 这是一条致命的信息
0 [main] ERROR Log4jTest  - 这是一条错误的信息
0 [main] WARN Log4jTest  - 这是一条警告的信息
0 [main] INFO Log4jTest  - 这是一条普通的信息
0 [main] DEBUG Log4jTest  - 这是一条调试的信息

注意:
BasicConfigurator.configure();
为log4j在不添加配置文件的情况下初始化默认日志配置信息,如果既没有默认配置信息,也没有配置文件

会报下面错误

未配置报错

4.2 日志级别

日志级别,就好比是日记本里的不同标记,用来区分信息的重要性。在log4j中,日志级别从低到高分为以下几种:

  1. TRACE
    :追踪级别,通常用来记录程序运行的详细轨迹,比如方法调用的顺序等。这个级别非常详细,一般在开发阶段或者调试时用得比较多。
  2. DEBUG
    :调试级别,用来记录程序的运行状态,比如变量的值、程序的流程等。当你需要深入了解程序的内部工作时,DEBUG级别就非常有用。
  3. INFO
    :信息级别,用来记录程序的正常运行状态,比如程序启动、配置信息、正常结束等。INFO级别的日志对用户和开发者了解程序的运行情况很有帮助。
  4. WARN
    :警告级别,用来记录一些可能引起问题的情况,但程序仍然可以继续运行。比如,程序遇到了一个不常见的情况,或者某个操作失败了但不影响大局。
  5. ERROR
    :错误级别,用来记录程序运行中的错误,这些错误通常会影响程序的正常功能,但程序可能还能继续运行。
  6. FATAL
    :致命级别,用来记录非常严重的错误,这些错误会导致程序完全无法继续运行。比如,程序的某个关键部分失败了,整个应用可能需要重启。

出了上面的,还有以下两个特殊级别

1. **OFF**: 用来关闭日志记录
1. **ALL**: 启用所有消息的日志记录

4.3 Log4j组件

  1. Logger
    :这个组件就像是日志的大脑,负责记录日志信息。你可以想象它是一个日记本的主人,决定哪些事情值得记录,哪些事情可以忽略。
  2. Appender
    :Appender就像是日记本的笔,它决定了日志信息要写到哪里。可以是控制台、文件、数据库,甚至是通过网络发送到远程服务器。每种Appender都有不同的用途和特点。
  3. Layout
    :Layout决定了日志的外观,也就是日志的格式。比如,你可以选择日志中包含时间、日志级别、发生日志的类名和方法名,以及日志的具体内容等。Layout就像是给日记本设计外观样式。
4.3.1 Logger

Log4j
中有一个特殊的
logger
叫做
root
,它是
logger
的根,其他的
logger
都会直接或者间接的继承自
root

入门示例中,我们通过
Logger.getLogger(Log4jTest.class);
获取的就是
root logger

name为
org.apache.commons
的logger会继承name为
org.apache
的logger

4.3.2 Appender

用来指定日志记录到哪儿,主要有以下几种

Appender类型 作用
ConsoleAppender 将日志输出到控制台
FileAppender 将日志输出到文件中
DailyRollingFileAppender 将日志输出到文件中,并且每天输出到一个日志文件中
RollingFileAppender 将日志输出到文件中,并且指定文件的大小,当文件大于指定大小,会生成一个新的日志文件
JDBCAppender 将日志保存到数据库中
4.3.3 Layout

用于控制日志内容输出格式,Log4j常用的有以下几种输出格式

日志格式器 说明
HTMLLayout 将日志以html表格形式输出
SimpleLayout 简单的日志格式输出,例如(info-message)
PatternLayout 最强大的格式化器,也是我们使用最多的一种,我们可以自定义输出格式

示例:下面我们通过
PatternLayout
格式化日志

@Test
public void testLog4jLayout(){
    //初始化日志配置信息,不需要配置文件
    BasicConfigurator.configure();
    //获取日志记录器
    Logger logger = Logger.getLogger(Log4jTest.class);
    Layout patternLayout = new PatternLayout("%d{yyyy-MM-dd HH:mm:ss} [%p] - %l - %m%n");// 将自定义的Layout应用到控制台Appender上
    ConsoleAppender consoleAppender = new ConsoleAppender(patternLayout);
    logger.addAppender(consoleAppender);
    // 记录日志
    logger.info("这是一条自定义格式的日志信息");

}
//输出
2024-08-04 13:55:35 [INFO] - Log4jTest.testLog4jLayout(Log4jTest.java:44) - 这是一条自定义格式的日志信息
占位符 说明
%m 输出代码中指定的日志信息
%p 输出优先级
%n 换行符
%r 输出自应用启用到输出log信息消耗的毫秒数
%c 输出语句所属的类全名
%t 输出线程全名
%d 输出服务器当前时间,%d
%l 输出日志时间发生的位置,包括类名、线程、及在代码中的函数 例如:
Log4jTest.testLog4jLayout(Log4jTest.java:44)
%F 输出日志消息产生时所在的文件夹名称
%L 输出代码中的行号
%5c category名称不足5位时,左边补充空格,即右对齐
%-5c category名称不足5位时,右边补充空格,即左对齐
.5c category名称大于5位时,会将左边多出的字符截取掉,小于5位时,以空格补充

4.4 通过配置文件配置日志

BasicConfigurator.configure();
上面代码中通过这段代码初始化日志配置信息,这一小节,我们通过配置文件来配置

通过看
LogManager
日志管理器源码,我们知道可以默认加载如下几种格式的配置文件(其中
log4j.xml

log4j.properties
是我们最常用的)

  • log4j.properties

  • log4j.xml

  • og4j.configuration

    等等

日志管理器源码

# 指定RootLogger顶级父元素默认配置信息
# 指定日志级别位INFO,使用的appender 位Console
log4j.rootLogger=INFO,Console
# 指定控制台日志输出appender
log4j.appender.Console = org.apache.log4j.ConsoleAppender
# 指定消息格式器 layout
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
# 指定消息内容格式
log4j.appender.Console.layout.conversionPattern =%d{yyyy-MM-dd HH:mm:ss} [%p] - %l - %m%n

或者

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%p] - %l - %m%n"/>
        </Console>
    </Appenders>
    <Loggers>
        <Root level="INFO">
            <AppenderRef ref="Console"/>
        </Root>
    </Loggers>
</Configuration>

4.5 各种日志输出示例

上面小节中已经说了控制台输出配置,由于篇幅原因,这里不再赘述

① 文件输出配置

# 指定RootLogger顶级父元素默认配置信息
# 指定日志级别位INFO,使用的appender 位Console
log4j.rootLogger=INFO,File
# 指定文件日志输出appender
log4j.appender.File = org.apache.log4j.FileAppender
#  指定日志文件名
log4j.appender.File.File=D:/logs/testxiezhr.log
#  指定是否在原有日志的基础添加新日志
log4j.appender.File.Append=true
# 指定消息格式器 layout
log4j.appender.File.layout=org.apache.log4j.PatternLayout
# 指定消息内容格式
log4j.appender.File.layout.conversionPattern =%d{yyyy-MM-dd HH:mm:ss} [%p] - %l - %m%n
# 指定日志文件编码格式
log4j.appender.File.encoding=UTF-8

②日志文件根据大小分割输出

# 指定RootLogger顶级父元素默认配置信息
# 指定日志级别位INFO,使用的appender 位Console
log4j.rootLogger=INFO,RollingFile
# 指定文件日志根据大小分割输出appender
log4j.appender.RollingFile = org.apache.log4j.RollingFileAppender
#  指定日志文件名
log4j.appender.RollingFile.File=D:/logs/testxiezhr.log
#  设置是否在重新启动服务时,在原有日志的基础添加新日志
log4j.appender.RollingFile.Append=true
# 设置最多保存的日志文件个数
log4j.appender.RollingFile.MaxBackupIndex=5
# 设置文件大小,超过这个值,就会再产生一个文件
log4j.appender.RollingFile.maximumFileSize=1

# 指定消息格式器 layout
log4j.appender.RollingFile.layout=org.apache.log4j.PatternLayout
# 指定消息内容格式
log4j.appender.RollingFile.layout.conversionPattern =%d{yyyy-MM-dd HH:mm:ss} [%p] - %l - %m%n
# 指定日志文件编码格式
log4j.appender.RollingFile.encoding=UTF-8

最终生成日志效果如下所示

按照日志大小切割文件

③ 日志文件根据日期分割

# 指定RootLogger顶级父元素默认配置信息
# 指定日志级别位INFO,使用的appender 位Console
log4j.rootLogger=INFO,DailyRollingFile
# 指定文件日志根据日期分割输出appender
log4j.appender.DailyRollingFile = org.apache.log4j.DailyRollingFileAppender
#  指定日志文件名
log4j.appender.DailyRollingFile.File=D:/logs/testxiezhr.log
#  设置是否在重新启动服务时,在原有日志的基础添加新日志
log4j.appender.DailyRollingFile.Append=true

# 指定消息格式器 layout
log4j.appender.DailyRollingFile.layout=org.apache.log4j.PatternLayout
# 指定消息内容格式
log4j.appender.DailyRollingFile.layout.conversionPattern =%d{yyyy-MM-dd HH:mm:ss} [%p] - %l - %m%n
# 指定日志文件编码格式
log4j.appender.DailyRollingFile.encoding=UTF-8

最终生成日志效果如下所示

日志文件根据日期进行分割

④ 自定义日志配置

当我们想定义自己的日志配置时,可以按照如下配置添加.例如:添加
com.xiezhr
,它也是继承自
rootLogger
,所以我们必须要添加

log4j.additivity.com.xiezhr=false
避免日志打印重复

# 指定RootLogger顶级父元素默认配置信息
# 指定日志级别位INFO,使用的appender 位Console
log4j.rootLogger=INFO,DailyRollingFile

# 自定义日志配置
log4j.logger.com.xiezhr=DEBUG,Console
# 设置日志叠加,这一句配置一定要添加,否则日志会重复输出
log4j.additivity.com.xiezhr=false

⑤ 将日志信息存入数据库

首先,我们新建一个testlog数据库,并在数据库下新建log日志表

CREATE TABLE `log` (
  `log_id` int(11) NOT NULL AUTO_INCREMENT,
  `project_name` varchar(255) DEFAULT NULL COMMENT '目项名',
  `create_date` varchar(255) DEFAULT NULL COMMENT '创建时间',
  `level` varchar(255) DEFAULT NULL COMMENT '优先级',
  `category` varchar(255) DEFAULT NULL COMMENT '所在类的全名',
  `file_name` varchar(255) DEFAULT NULL COMMENT '输出日志消息产生时所在的文件名称 ',
  `thread_name` varchar(255) DEFAULT NULL COMMENT '日志事件的线程名',
  `line` varchar(255) DEFAULT NULL COMMENT '号行',
  `all_category` varchar(255) DEFAULT NULL COMMENT '日志事件的发生位置',
  `message` varchar(4000) DEFAULT NULL COMMENT '输出代码中指定的消息',
  PRIMARY KEY (`log_id`)
);

其次,新建
JDBCAppender
,并且为
JDBCAppender
设置数据库连接信息,具体代码如下

@Test
public void testLog4j2db(){
    //初始化日志配置信息,不需要配置文件
    BasicConfigurator.configure();
    //获取日志记录器
    Logger logger = Logger.getLogger(Log4jTest.class);
    // 新建JDBCAppender
    JDBCAppender jdbcAppender = new JDBCAppender();
    jdbcAppender.setDriver("com.mysql.cj.jdbc.Driver");
    jdbcAppender.setURL("jdbc:mysql://localhost:3308/testlog?useSSL=false&serverTimezone=UTC");
    jdbcAppender.setUser("root");
    jdbcAppender.setPassword("123456");
    jdbcAppender.setSql("INSERT INTO log(project_name,create_date,level,category,file_name,thread_name,line,all_category,message) values('晓凡日志测试','%d{yyyy-MM-dd HH:mm:ss}','%p','%c','%F','%t','%L','%l','%m')");

    logger.addAppender(jdbcAppender);
    // 记录日志
    logger.info("这是一条自定义格式的日志信息");
    logger.error("这是一条自定义格式的错误日志信息");
}

最后,运行代码,来看一下效果

日志信息已经村到数据库中了

五、JCL日志门面

何为日志门面,我们在第二小节中已经介绍过了,这里就不多说了。

日志门面的引入,使得我们可以面向接口开发,不再依赖具体的实现类,减小代码耦合。

JCL
全称
Jakarta Commons Logging
是Apache提供的一个通用日志
API
,
JCL
中自带一个日志实现
simplelog
,不过这个功能非常简单

jcl实现图

5.1 JCL快速入门

① LCL的两个抽象类

  • Log: 基本日志记录器
  • LogFactory: 负责创建Log具体实例,如果时log4j,则创建log4j的实例,如果时jul则创建jul实例

② 示例代码

引入依赖

<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
</dependency>

基本代码

我们没有导入任何日志实现,所以这里默认使用jdk自带
JUL
来实现日志

@Test
public void test(){

    Log log = LogFactory.getLog(JclTest.class);

    log.error("这是一条error");
    log.warn("这是一条warn");
    log.info("这是一条info");
    log.debug("这是一条debug");
    log.trace("这是一条trace");
}

日志输出

5.2 快速切换Log4j日志框架

① 导入
log4j
日志依赖

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

② 添加
log4j.properties
配置文件

# 指定RootLogger顶级父元素默认配置信息
# 指定日志级别位INFO,使用的appender 位Console
log4j.rootLogger=INFO,Console
# 指定控制台日志输出appender
log4j.appender.Console = org.apache.log4j.ConsoleAppender
# 指定消息格式器 layout
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
# 指定消息内容格式
log4j.appender.Console.layout.conversionPattern =%d{yyyy-MM-dd HH:mm:ss} [%p] - %l - %m%n

③ 测试日志输出

@Test
public void testJclLog4j(){

    Log log = LogFactory.getLog(JclLog4jTest.class);
    log.error("这是一条error");
    log.warn("这是一条warn");
    log.info("这是一条info");
    log.debug("这是一条debug");
    log.trace("这是一条trace");
}

日志输出如下:

log4j日志输出

我们可以看到,使用了
JCL
日志门面之后,我们从
simplelog
日志框架切换到
log4j
日志框架,没有改过代码。

六、SLF4j日志门面

SLF4j
全称是
Simple Logging Facade For Java
Java简单的日志门面 和上一小节说到的
JCL
干的一样的活。

在现目前的大多数Java项目中,日志框架基本上会选择
slf4j-api
作为门面,配上具体实现框架
logback

log4j
等使用

SLF4j
是目前市面上最流行的日志门面,主要提供了以下两个功能

  • 日志框架的绑定
  • 日志框架的桥接

6.1 快速入门

① 添加依赖

<!--添加日志门面sl4j-->
 <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>2.0.13</version>
 </dependency>
 <!--添加slf4j 自带的简单日志实现-->
 <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-simple</artifactId>
      <version>2.0.13</version>
  </dependency>

②日志输出

 //申明日志对象
public final  static Logger logger = LoggerFactory.getLogger(Slf4jTest.class);
@Test
public void testSlf4j(){
    //打印日志
    logger.error("这是error日志");
    logger.warn("这是warn日志");
    logger.info("这是info日志");
    logger.debug("这是debug日志");
    logger.trace("这是trace日志");

    //使用占位符输出日志信息
    String name = "晓凡";
    Integer age = 18;
    logger.info("{},今年{}岁", name, age);

    //将系统异常写入日志
    try {
        int i = 1/0;
    }catch (Exception e){
        logger.error("执行出错", e);
    }

}

上面代码输出日志如下

日志输出

6.2 SLF4j 日志绑定功能

6.2.1 日志绑定原理

下图是从官网薅下来的
slf4j
日志绑定图,对了,官网在这https://www.slf4j.org/

logback

小伙伴看到上图可能会有点懵,全是英文,看不懂。

一脸懵逼

于是乎,晓凡简单翻译了一下,如下如所示

slf4j实现原理图

  • 只导入日志门面,没导入日志实现,不会进行日志输出
  • logback

    simplelog

    no-operation
    框架遵循
    SLF4j
    规范 导入jar包即可使用
  • log4j

    JUL
    属于比较古老日志框架,不遵循
    SLF4j
    规范,需要引入适配器才能使用
  • 当我们导入
    slf4j-nop
    后将不会使用任何日志框架
6.2.2 绑定logback日志框架

① 引入logback依赖

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.4.14</version>
</dependency>

② 日志输出

快速入门中代码不变,运行后,采用logback日志框架输入日志如下所示

logback日志输出

6.2.3 绑定
slf4j-nop

① 引入依赖

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-nop</artifactId>
    <version>2.0.13</version>
</dependency>

② 此时控制台将不会输出任何日志

6.2.4 使用适配器绑定
log4j
日志框架

① 导入依赖

<!--log4j适配器-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>2.0.13</version>
</dependency>
<!--log4j日志框架依赖-->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

② 添加log4j.properties配置文件

# 指定RootLogger顶级父元素默认配置信息
# 指定日志级别位INFO,使用的appender 位Console
log4j.rootLogger=INFO,Console
# 指定控制台日志输出appender
log4j.appender.Console = org.apache.log4j.ConsoleAppender
# 指定消息格式器 layout
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
# 指定消息内容格式
log4j.appender.Console.layout.conversionPattern =%d{yyyy-MM-dd HH:mm:ss} [%p] - %l - %m%n

③ 代码不变,日志输出如下

log4j日志输出

6.2.5 使用适配器绑定JUL日志框架

① 引入依赖

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-jdk14</artifactId>
    <version>2.0.13</version>
</dependency>

② 代码不变,日志输出如下

jul日志输出结果

6.3 SLF4j日志桥接

6.3.1 使用场景

如果你的项目中已经使用了
Log4j 1.x
等老的日志框架,但你想迁移到使用
SLF4J

API
,这时候你可以使用
SLF4J

Log4j 1.x
桥接器来平滑过渡

6.3.2 桥接原理

桥接器原理

上图为SLF4j官网提供的桥接原理图,从图中,我们可以看到,只需要引入不同的桥接器
log4j-over-slf4j

jul-to-slf4j

jcl-over-slf4j

就可以实现在
不改变原有代码
的情况下,将日志从
log4j

jul

jcl
迁移到
slf4j
+
logback
日志组合

6.3.3 桥接步骤

下面以
Log4j 1.x
迁移到
slf4j
+
logback
日志组合为例

  1. 去除老的日志框架
    Log4j 1.x
    依赖


去除老项目中的日志依赖

  1. 添加
    SLF4J
    提供的桥接组件

    引入桥接器

  2. 为项目添加
    SLF4J
    的具体实现

引入新的日志实现

七、Logback日志框架

官网:
https://logback.qos.ch/index.html

7.1 快速入门

① 添加依赖

<!--添加日志门面SLF4j依赖-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>2.0.13</version>
</dependency>
<!--添加Logback日志实现依赖-->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.4.14</version>
</dependency>

② 打印日志代码

public class LogbackTest {

    private static final Logger logger = LoggerFactory.getLogger(LogbackTest.class);

    @Test
    public void testLogbackQuick(){
        logger.error("这是一个错误日志");
        logger.warn("这是一个警告日志");
        logger.info("这是一个信息日志");
        logger.debug("这是一个调试日志");
        logger.trace("这是一个跟踪日志");
    }
}

Logback打印日志

7.2 Logback配置

Logback可以通过编程式配置(添加配置类的方式),也可以通过配置文件配置。

配置文件是日常开发中最常用的,我们这里就以这种方式配置,如果对配置文件感兴趣的小伙伴可自行到官网查看

7.2.1 Logback 包含哪些组件?
  1. Logger
    :日志记录器,用来记录不同级别的日志信息,比如错误、警告、信息、调试和追踪。
  2. Appender
    :指定日志信息输出到不同的地方。比如,你可以设置一个Appender将日志输出到控制台,另一个Appender将日志写入文件,或者发送到远程服务器。
  3. Encoder
    :如果你使用的是文件Appender,Encoder就是用来定义日志文件内容格式的。比如,你可以选择日志的格式是简单文本还是XML。
  4. Layout
    :老版本的
    Logback
    中用来定义日志格式的组件。在新版本中,Encoder已经取代了Layout的功能。
  5. Filter
    :指定特定的规则来过滤日志信息,比如只记录错误以上的日志,或者只记录包含特定关键字的日志。
  6. Configuration
    :用来配置
    Logback
    的设置,比如设置日志级别、Appender的类型和参数等。配置可以通过
    XML

    JSON
    或者
    Groovy
    脚本来完成。
7.2.2 可以有哪些文件格式进行配置?

Logback会依次读取以下类型配置文件

  • logback.groovy

  • logback-test.xml

  • logback.xml
    (
    最常用的
    )

    如果均不存在会采用默认配置

7.2.3 添加一个
ConsoleAppender
控制台日志输出配置

配置文件

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!--添加一个名字为pattern的属性 用来设置日志输出可是-->
    <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
    <property name="pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %c [%thread]%-5level %msg%n" />

    <!--输出到控制台-->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--引用上面配置好的pattern属性-->
            <pattern>${pattern}</pattern>
        </encoder>
    </appender>
    <!--设置日志级别-->
    <root level="ALL">
        <!--引用上面配置好的consoleAppender将日志输出到控制台-->
        <appender-ref ref="console" />
    </root>
</configuration>

日志输入如下

日志输出

日志输出格式:在前面几个日志框架中我们已经介绍过,大同小异。这里简单说下常用的几种

符号 含义
%d{pattern} 格式化日期
%m或者%msg 日志信息
%M method(方法)
%L 行号
%c 完整类名称
%thread 线程名称
%n 换行
%-5level 日志级别,并且左对齐
7.2.4 添加一个
FileAppender
将日志输出到文件

配置文件

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!--添加一个名字为pattern的属性 用来设置日志输出可是-->
    <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
    <property name="pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %c [%thread]%-5level %msg%n" />

    <!--设置日志文件存放路径-->
    <property name="log_file" value="d:/logs"></property>

    <!--输出到文件-->
    <appender name="file" class="ch.qos.logback.core.FileAppender">
        <encoder>
            <!--引用上面配置好的pattern属性-->
            <pattern>${pattern}</pattern>
        </encoder>
        <!--被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值。-->
        <file>${log_file}/logback.log</file>
    </appender>
    <!--设置日志级别-->
    <root level="ALL">
        <!--引用上面配置好的FileAppender将日志输出到文件-->
        <appender-ref ref="file" />
    </root>
</configuration>

日志输出如下

将日志输出到文件中

7.2.5 生成html格式appender对象
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!--添加一个名字为pattern的属性 用来设置日志输出可是-->
    <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
    <property name="pattern" value="%-5level%d{yyyy-MM-dd HH:mm:ss}%c%M%L%thread%m"/>

    <!--设置日志文件存放路径-->
    <property name="log_file" value="d:/logs"></property>

    <!--输出到文件-->
    <appender name="htmlFile" class="ch.qos.logback.core.FileAppender">

        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">

            <layout class="ch.qos.logback.classic.html.HTMLLayout">
                <!--引用上面配置好的pattern属性-->
                <pattern>${pattern}</pattern>
            </layout>
        </encoder>
        <!--被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值。-->
        <file>${log_file}/logback.html</file>
    </appender>
    <!--设置日志级别-->
    <root level="ALL">
        <!--引用上面配置好的FileAppender将日志输出到文件-->
        <appender-ref ref="htmlFile" />
    </root>
</configuration>

日志输出:

d:/logs
目录下生成一个
logback.html
文件

image-20240810171050844

7.3 Logback 日志拆分压缩 ⭐

在生产环境中对日志进行按时间、日志大小拆分 且压缩日志
非常非常重要
,所以单独拿出来说一说

配置文件

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!--添加一个名字为pattern的属性 用来设置日志输出可是-->
    <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
    <property name="pattern" value="[%-5level] %d{yyyy-MM-dd HH:mm:ss} %c %M %L [%thread] %m %n" />

    <!--设置日志文件存放路径-->
    <property name="log_file" value="d:/logs"></property>

    <!--输出到文件-->
    <appender name="rollFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--引用上面配置好的pattern属性-->
            <pattern>${pattern}</pattern>
        </encoder>
        <!--被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值。-->
        <file>${log_file}/roll_logback.log</file>
        <!--滚动记录文件:根据时间来制定滚动策略-->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!--日志文件输出的文件名-->
            <fileNamePattern>${log_file}/roll_logback.%d{yyyy-MM-dd}.log%i.gz</fileNamePattern>
            <!--指定文件拆分大小-->
            <maxFileSize>1MB</maxFileSize>
            <!--日志文件保留天数-->
            <MaxHistory>3</MaxHistory>
        </rollingPolicy>
    </appender>
    <!--设置日志级别-->
    <root level="ALL">
        <!--引用上面配置好的FileAppender将日志输出到文件-->
        <appender-ref ref="rollFile" />
    </root>
</configuration>

日志滚动输出:
按照日期和文件大小进行拆分

按日期和文件大小进行拆分

7.4 异步日志

我们先来解释下什么是异步日志?

我们将日志输出到文件中,这样会涉及到大量
io
操作,非常耗时,如果需要输出大量的日志,就可能影响正常的主线程业务逻辑。

为了解决这问题,异步日志就出现了。日志信息不是直接写入到日志文件或者控制台,而是先发送到一个队列里,

然后由一个专门的线程去处理这些日志信息的写入工作。

这样做的好处是可以减少日志记录对主程序运行的影响,提高程序的效率。

7.4.1 不加异步日志
private static final Logger logger = LoggerFactory.getLogger(LogbackTest.class);

    @Test
    public void testLogbackQuick(){

        //日志输出
        logger.error("这是一个错误日志");
        logger.warn("这是一个警告日志");
        logger.info("这是一个信息日志");
        logger.debug("这是一个调试日志");
        logger.trace("这是一个跟踪日志");

        //这里模拟业务逻辑
        System.out.println("晓凡今年18岁了");
        System.out.println("晓凡的个人博客是:www.xiezhrspace.cn");
        System.out.println("晓凡的个人公众号是:程序员晓凡");
        System.out.println("晓凡的个人微信是:xie_zhr");
        System.out.println("欢迎关注晓凡,持续输出干货!!!!!");
    }

输出结果:

未加异步日志

从上面控制台输出看,只有当日志输出完成之后我们的业务逻辑代码才被执行。如果日志耗时比较长,非常影响效率

7.4.2 添加异步日志

我们只需在原来的配置文件中添加如下关键配置

<!--添加异步日志配置-->
<appender name="async" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="console" />
</appender>
<root level="ALL">
    <!--引用上面配置好的consoleAppender将日志输出到控制台-->
    <appender-ref ref="console" />
    <!--引用上面配置好的asyncAppender将日志输出到控制台-->
    <appender-ref ref="async" />        
</root>

日志输出效果:

异步日志输出效果

从上面日志日志输出看,不再是日志输出完再进行业务逻辑代码执行,而是异步执行了

八、Log4j2日志框架

官网:
https://logging.apache.org/log4j/2.x/

Log4j2

Log4j
的升级版,参考了
Logback
的一些优秀设计,修复了一些bug,性能和功能都带来了极大提升

主要体现在以下几个方面

  • 性能提升:
    Log4j2
    在多线程环境下表现出更高的吞吐量,比
    Log4j 1.x

    Logback
    高出10倍

  • 异步日志

    Log4j2
    支持异步日志记录,可以通过
    AsyncAppender

    AsyncLogger
    实现。异步日志可以减少日志记录对主程序性能的影响,尤其是在高并发场景下

  • 自动重载配置

    Log4j2
    支持动态修改日志级别而不需要重启应用,这是借鉴了
    Logback
    的设计

  • 无垃圾机制

    Log4j2
    大部分情况下使用无垃圾机制,避免因频繁的日志收集导致的
    JVM GC2

  • 异常处理

    Log4j2
    提供了异常处理机制,Appender 中的异常可以被应用感知到,而
    Logback
    中的异常不会被应用感知

Log4j2
有这么多优势,所以在未来
SLF4j
+
Log4j2
组合

8.1 快速入门

Log4j2不仅仅是日志实现,同时也是日志门面。在快速入门中,我们就使用Log4j2作为日志门面和日志实现来快速入门

8.1.1 添加依赖
<!--添加log4j2日志门面API-->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.23.1</version>
</dependency>
<!--添加log4j2日志实现-->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.23.1</version>
</dependency>
8.1.2 添加日志实现代码
public class Log4j2Test {

    private static final Logger logger = LogManager.getLogger(Log4j2Test.class);
    @Test
    public void Log4j2Test(){
        logger.fatal("这是一条致命信息");
        logger.error("这是一条错误信息");
        logger.warn("这是一条警告信息");
        logger.info("这是一条一般信息");
        logger.debug("这是一条调试信息");
        logger.trace("这是一条追踪信息");

    }
}

日志输出结果如下

log4j2 日志输出结果

8.2 使用slf4j+log4j2组合

前面我们提到
SLF4j
+
Log4j2
组合会是未来日志发展的大趋势,所以接下来我们就使用这个组合来输出日志

导入依赖

<!--添加log4j2日志门面API-->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.23.1</version>
</dependency>
<!--添加log4j2日志实现-->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.23.1</version>
</dependency>

<!--添加slf4j作为日志门面,使用log4j2作为日志实现-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>2.0.13</version>
</dependency>
<!--添加log4j2与slf4j的桥接器-->    
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>2.23.1</version>
</dependency>

日志输出代码

public class Log4j2Test {
	//这里我们换成了slf4j的门面接口
    private static final Logger logger = LoggerFactory.getLogger(Log4j2Test.class);

    @Test
    public void Log4j2Test(){
        logger.error("这是一条错误信息");
        logger.warn("这是一条警告信息");
        logger.info("这是一条一般信息");
        logger.debug("这是一条调试信息");
        logger.trace("这是一条追踪信息");

    }
}

日志输出效果

slf4j+log4j2组合日志输出

8.3 Log4j2配置

log4j2 默认加载classpath 下的 log4j2.xml 文件中的配置。

下面通过log4j2.xml 配置文件进行测试,配置大同小异,这里就不一一说明了,给出完整的配置

<?xml version="1.0" encoding="UTF-8" ?>
<!--status="warn" 日志框架本身的输出日志级别,可以修改为debug    monitorInterval="5" 自动加载配置文件的间隔时间,不低于 5秒;生产环境中修改配置文件,是热更新,无需重启应用 -->
<configuration status="warn" monitorInterval="5">
    <!--集中配置属性进行管理    使用时通过:${name}  -->
    <properties>
        <property name="LOG_HOME">D:/logs</property>
    </properties>
    <!--日志处理 -->
    <Appenders>
        <!--控制台输出 appender,SYSTEM_OUT输出黑色,SYSTEM_ERR输出红色 -->
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] [%-5level] %c{36}:%L --- %m%n" />
        </Console>
        <!--日志文件输出 appender -->
        <File name="file"  fileName="${LOG_HOME}/file.log">
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %m%n" />
        </File>
        <!-- 使用随机读写流的日志文件输出 appender,性能提高 -->
        <RandomAccessFile name="accessFile" fileName="${LOG_HOME}/access.log">
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %m%n" />
        </RandomAccessFile>
        <!--按照一定规则拆分的日志文件的appender -->
        <!-- 拆分后的文件 -->
        <!-- 拆分后的日志文件命名规则:log-debug.log、log-info.log、log-error.log -->
        <RollingFile name="rollingFile" fileName="${LOG_HOME}/rolling.log"
                     filePattern="${LOG_HOME}/$${date:yyyy-MM-dd}/rolling-%d{yyyy-MM-dd}-%i.log.gz">
            <!-- 日志级别过滤器 -->
            <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY" />
            <!-- 日志消息格式 -->
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %msg%n" />
            <Policies>
                <!-- 在系统启动时,出发拆分规则,生产一个新的日志文件 -->
                <OnStartupTriggeringPolicy  />
                <!-- 按照文件大小拆分,1MB -->
                <SizeBasedTriggeringPolicy size="1MB" />
                <!--按照时间节点拆分,规则根据filePattern定义的 -->
                <TimeBasedTriggeringPolicy />
            </Policies>
            <!-- 在同一个目录下,文件的个限定为 10个,超过进行覆盖 -->
            <DefaultRolloverStrategy max="10" />
        </RollingFile>
    </Appenders>
    <!-- logger 定义 -->
    <Loggers>
        <!--使用 rootLogger 配置 日志级别 level="trace" -->
        <Root level="trace">
            <!--指定日志使用的处理器 -->
            <AppenderRef ref="Console" />
<!--            <AppenderRef ref="file"/>-->
            <AppenderRef ref="rollingFile" />
            <AppenderRef ref="accessFile" />
        </Root>
    </Loggers>
</configuration>

日志输出如下

日志按天拆分

下面的截图为2024-08-11的日志按日志文件大小1MB拆分成10个并进行压缩,拆分满10个文件后新日志会覆盖旧日志,其他天的类似

按日志大小和文件数进行拆分

8.4 Log4j2 异步日志

Log4j2
最大的特点就是异步日志,就因为异步日志的存在,将性能提升了好多。

下图是官网给的性能对比图,从图中我们可以看出在
全局异步模式
(Loggers all async) 和
混合异步模式
(Loggers mixed sync/async)

性能简直将
Logback

Log4j
日志框架甩了一条街。

至于什么时全局异步模式和混合异步模式?我们会在后面详细说明

日志框架性能比较

8.4.1 陌生名词解释
  • 同步日志
    :想象一下你手里有一堆信件要写,每写一封信你都得亲自动手,写完后才能去做别的事情。在这个过程中,你得一封一封地写,不能同时干其他事,这就类似于同步日志。在程序中,同步日志意味着每次记录日志时,程序都得停下来,等待日志写完了才能继续执行其他任务。这样做的好处是不会丢信(日志),但坏处是写信(记录日志)这个过程如果太慢,就会耽误你做其他事情(程序运行)

  • 异步日志
    :如果你特别忙,你可能会找个助手来帮你写信。你只需要告诉他要写什么,然后就可以继续忙自己的事情,而助手会帮你把信写好并寄出去。这个过程就像是异步日志。在程序中,异步日志意味着程序可以把要记录的日志信息交给一个专门的“助手”(通常是另外的线程或进程),然后程序就可以继续执行其他任务,而不需要等待日志写完。这样做的好处是可以更快地处理任务,不会耽误正事儿,但偶尔可能会有一两封信(日志)因为意外情况没有寄出去。

  • 全局异步
    : 所有日志记录都采用异步的方式记录

  • 混合异步
    :以在应用中同时使用同步日志和异步日志,这使得日志配置更加灵活

8.4.2 同步日志与异步日志
  1. 同步日志流程

同步日志流程

2、异步日志流程

异步日志流程图

8.5 异步日志配置

异步日志的实现一共有两种方式

  • AsyncAppender
    [
    生产上几乎不使用,因为性能低下
    ]

  • AsyncLogger
    [
    生产上用得多,因为性能高
    ]


    • 全局异步
    • 混合异步

第一种方式因为用的不多性能也不够好,所以这里就不说了,我们以第二种配置来具体说一说

不管采用哪种方式,首先都要引入异步依赖

<!--异步日志依赖-->
<dependency>
    <groupId>com.lmax</groupId>
    <artifactId>disruptor</artifactId>
    <version>3.4.4</version>
</dependency>


全局异步

只需在
resources
下添加
log4j2.component.properties
,具体内容如下

Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector

全局异步

日志输出结果

全局异步日志开启,

② 混合异步配置

首先,我们需要关闭全局异步配置,将上面添加的
log4j2.component.properties
内容注释即可

log4j2.xml
配置

<?xml version="1.0" encoding="UTF-8" ?>
<!--status="warn" 日志框架本身的输出日志级别,可以修改为debug    monitorInterval="5" 自动加载配置文件的间隔时间,不低于 5秒;生产环境中修改配置文件,是热更新,无需重启应用 -->
<configuration status="debug" monitorInterval="5">
    <!--集中配置属性进行管理    使用时通过:${name}    -->
    <properties>
        <property name="LOG_HOME">D:/logs</property>
    </properties>
    <!--日志处理 -->
    <Appenders>
        <!--控制台输出 appender,SYSTEM_OUT输出黑色,SYSTEM_ERR输出红色 -->
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] [%-5level] %c{36}:%L --- %m%n" />
        </Console>
        <!--日志文件输出 appender -->
        <File name="file" fileName="${LOG_HOME}/file.log">
            <!-- <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %m%n" />-->
            <PatternLayout pattern="%d %p %c{1.} [%t] %m%n" />
        </File>
        <Async name="Async">
            <AppenderRef ref="file" />
        </Async>
    </Appenders>
    <!--logger 定义 -->
    <Loggers>
        <!--自定义 logger 对象  includeLocation="false" 关闭日志记录的行号信息,开启的话会严重影响异步输出的性能            additivity="false" 不再继承 rootlogger对象         -->
        <AsyncLogger name="com.xiezhr" level="trace" includeLocation="false" additivity="false">
            <AppenderRef ref="Console" />
        </AsyncLogger>
        <!-- 使用 rootLogger 配置 日志级别 level="trace" -->
        <Root level="trace">
            <!-- 指定日志使用的处理器 -->
            <AppenderRef ref="Console" />
            <!-- 使用异步 appender -->
            <AppenderRef ref="Async" />
        </Root>
    </Loggers>
</configuration>

输出结果:

开启混合异步日志,控制台输出采用异步日志

注意事项:

  • 上面配置
    AsyncAppender
    、全局配置、混合配置 不能同时出现,否则将影响日志性能
  • includeLocation="false"
    关闭日志记录的行号信息 配置一定要加上,否则会降低日志性能

九、阿里巴巴日志规约

通过上面八小节我们对Java日志框架应该非常熟悉了,并且也知道怎么使用了。但在日志开发中,使用日志还是有写规约需要我们去遵守。

下面式阿里巴巴Java开发手册中的日志规约

❶【
强制
】应用中不可直接使用日志系统(
Log4j

Logback
)中的
API
,而应依赖使用日志框架(
SLF4J

JCL--Jakarta Commons Logging
)中的
API
,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。

说明:日志框架(SLF4J、JCL--Jakarta Commons Logging)的使用方式(推荐使用 SLF4J)

1)使用
SLF4J

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Test.class);
  1. 使用
    JCL
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
private static final Log log = LogFactory.getLog(Test.class);

❷【
强制
】所有日志文件至少保存 15 天,因为有些异常具备以“周”为频次发生的特点。对于当天日志,以“应用名
.log
”来保存,

保存在”
/home/admin/应用名/logs/
“目录下,过往日志格式为:
{logname}.log.{保存日期}
,日期格式:
yyyy-MM-dd

正例
:以 aap 应用为例,日志保存在
/home/admin/aapserver/logs/aap.log
,历史日志名称为
aap.log.2021-03-23

❸【
强制
】根据国家法律,网络运行状态、网络安全事件、个人敏感信息操作等相关记录,留存的日志不少于六个月,并且进行网络多机备份。

❹【
强制
】应用中的扩展日志(如打点、临时监控、访问日志等)命名方式:
appName_logType_logName.log

  • logType
    :日志类型,如
    stats/monitor/access
    等;

  • logName
    :日志描述。

    这种命名的好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找。

说明:推荐对日志进行分类,如将错误日志和业务日志分开存放,便于开发人员查看,也便于通过日志对系统进行及时监控。

正例

mppserver
应用中单独监控时区转换异常,如:

mppserver_monitor_timeZoneConvert.log

❺ 【
强制
】在日志输出时,字符串变量之间的拼接使用占位符的方式。

说明:因为 String 字符串的拼接会使用
StringBuilder

append()
方式,有一定的性能损耗。使用占位符仅是替换动作,可以有效提升性能。

正例:

logger.debug("Processing trade with id: {} and symbol: {}", id, symbol);

❻【
强制
】对于
trace
/
debug
/
info
级别的日志输出,必须进行日志级别的开关判断。

说明:虽然在
debug(参数)
的方法体内第一行代码
isDisabled(Level.DEBUG_INT)
为真时(Slf4j 的常见实现Log4j 和 Logback),就直接
return
,但是参数可能会进行字符串拼接运算。此外,如果
debug(getName())
这种参数内有
getName()
方法调用,无谓浪费方法调用的开销。

正例:

// 如果判断为真,那么可以输出 trace 和 debug 级别的日志
if (logger.isDebugEnabled()) {
    logger.debug("Current ID is: {} and name is: {}", id, getName());
}

❼【
强制
】避免重复打印日志,浪费磁盘空间,务必在日志配置文件中设置
additivity=false

正例:

<logger name="com.taobao.dubbo.config" additivity="false">

❽ 【强制】生产环境禁止直接使用
System.out

System.err
输出日志或使用
e.printStackTrace()
打印异常堆栈 。

说明:标准日志输出与标准错误输出文件每次
Jboss
重启时才滚动,如果大量输出送往这两个文件,容易造成文件大小超过操作系统大小限制。

❾ 【强制】异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字
throws
往上抛出。

正例:

logger.error("inputParams:{} and errorMessage:{}", 各类参数或者对象 toString(), e.getMessage(), e);

❿ 【
强制
】日志打印时禁止直接用 JSON 工具将对象转换成
String

说明:如果对象里某些
get
方法被覆写,存在抛出异常的情况,则可能会因为打印日志而影响正常业务流程的执行。

正例:

打印日志时仅打印出业务相关属性值或者调用其对象的
toString()
方法。

⓫ 【
推荐
】谨慎地记录日志。生产环境禁止输出
debug
日志;有选择地输出
info
日志;

如果使用
warn
来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。

说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?

⓬ 【
推荐
】可以使用
warn
日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。

说明:如非必要,请不要在此场景打出
error
级别,避免频繁报警。 注意日志输出的级别,
error
级别只记录系统逻辑出错、异常或者重要的错误信息。

⓭ 【
推荐
】尽量用英文来描述日志错误信息,如果日志中的错误信息用英文描述不清楚的话使用中文描述即可,否则容易产生歧义。

说明:国际化团队或海外部署的服务器由于字符集问题,使用全英文来注释和描述日志错误信息。

本期内容到这儿就结束了
★,°
:.☆( ̄▽ ̄)/$:
.°★
。 希望对您有所帮助

我们下期再见 ヾ(•ω•`)o (●'◡'●)