2024年8月

一、准备工作

1、系统要求

docker要求centos的内核版本不低于3.10。centos7满足最低内核要求。

检查当前的内核版本
[root@zspc /]# uname -r
3.10.0-1160.el7.x86_64

可以看到我当前的内核版本为3.10,符合最低内核要求。

2、卸载旧版本

如果之前安装过Docker的话,需要先卸载。

执行以下命令即可:

yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-selinux \
                  docker-engine-selinux \
                  docker-engine \
                  docker-ce

二、安装Docker

1、安装依赖包

yum install -y yum-utils \
           device-mapper-persistent-data \
           lvm2 --skip-broken

2、更新本地镜像源

由于国内网络原因,所以这里使用阿里的docker源

# 设置docker镜像源
yum-config-manager \
    --add-repo \
    https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
    
sed -i 's/download.docker.com/mirrors.aliyun.com\/docker-ce/g' /etc/yum.repos.d/docker-ce.repo

yum makecache fast

3、安装Docker

yum install docker-ce docker-ce-cli containerd.io

安装过程中如果有提示,就选y。

4、验证是否安装成功

[root@zspc /]# docker -v
Docker version 26.1.4, build 5650f9b

三、启动Docker

一般情况下,启动Docker只需要关闭防火墙即可,但这是极其不安全的行为,所以要想在启用防火墙的情况下运行Docker,就要配置防火墙以允许Docker的相关流量。

1、检查防火墙

执行以下命令检查防火墙状态,可以看到防火墙正在运行

[root@zspc /]# systemctl status firewalld
● firewalld.service - firewalld - dynamic firewall daemon
   Loaded: loaded (/usr/lib/systemd/system/firewalld.service; enabled; vendor preset: enabled)
   Active: active (running) since Tue 2024-08-13 11:28:52 CST; 24h ago
     

2、配置防火墙

Docker 使用了一种名为 bridge 的网络类型,需要在防火墙中打开这种网络类型。

firewall-cmd --permanent --zone=public --add-masquerade

Docker 默认使用 172.17.0.0/16 网段,需要在防火墙中打开这个网段。

firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 4 -i docker0 -j ACCEPT

重新加载防火墙以应用新的规则

firewall-cmd --reload

4、启动Docker

systemctl start docker

设置开机自启动

systemctl enable docker

验证是否启动成功

执行 docker ps 命令,出现如下结果,即表示Docker启动成功。

[root@zspc /]# docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

现象

群里反馈管理后台登录不上了,我一访问,整个界面空白,没有提示,打开 F12,发现控制台提示 js、css 等静态资源报 net::ERR_HTTP2_PROTOCOL_ERROR,客户端可以下载到服务端资源,第一次碰到这个,StackOverflow 走起

net::ERR_HTTP2_PROTOCOL_ERROR 是关于什么的?

可能出现的问题非常多,包括磁盘满、nginx 配置问题、请求头问题、浏览器问题、cdn 问题。因为这个服务没有更新过,也没人动过配置,所以不太可能是 nginx、服务的问题,另外每次下载 js 文件,似乎都不是完整的,虽然服务器都有做出响应。

所以初步判断是服务器出了问题,但是没有配置 ssh,登录阿里云后台的手机也在公司,没办法看到服务器监控。

此时发现多刷新几次,利用浏览器的本地缓存,首页还是能访问出来的,但是登录功能无法响应,如果填了错误的验证码,可以立马返回错误信息。说明 MySQL 写受到了影响,基本上可以是断定服务器的磁盘、内存问题。

第二天到了公司看控制台监控,好家伙磁盘占用 99%

刷新了一下管理后台,响应非常非常的慢;先清理空间再验证是否这个问题

解决

列出所有挂载的文件系统,并显示每个文件系统的总空间、已用空间、可用空间和使用率。

df -h

#Filesystem      Size  Used Avail Use% Mounted on
#udev             16G     0   16G   0% /dev
#tmpfs           3.1G  724K  3.1G   1% /run
#/dev/vda1        99G   94G  343M 100% /
#tmpfs            16G     0   16G   0% /dev/shm
#tmpfs           5.0M     0  5.0M   0% /run/lock
#tmpfs            16G     0   16G   0% /sys/fs/cgroup
#tmpfs           3.1G     0  3.1G   0% /run/user/0

这时候要注意了,如果直接从根目录递归查找,会消耗大量的 CPU 和 IO 资源,可能会导致服务器变得非常慢,甚至暂时无法响应,所以要一层一层来

# /* 换成对应的目录即可
du -sh /* | sort -rh | head -n 10

最终找出了两个目录,nacos 的 access 日志 和 MySQL binlog 日志,总计 80G 空间,其中大部分都是七天前的,可处理

对于 nacos 的日志,直接删了之前的就好。而 MySQL 的 binlog 文件,建议从控制台执行命令

# 删除 七天前的 binlog
PURGE BINARY LOGS BEFORE NOW() - INTERVAL 7 DAY;

最终,处理了 40% 的空间

验证

多次强制刷新前端页面,静态资源和接口的响应速度也变快了。

接下来看一下昨天的日志,MySQL 除了写数据时获取不到锁,没有其他报错

包括 nginx 日志, /var/log/syslog 系统日志,/var/log/mysql/error.log 提示空间不足

2024-08-13T02:27:02.537301Z 1256492 [ERROR] [MY-000035] [Server] Disk is full writing './binlog.000556' (OS errno 28 - No space left on device). Waiting for someone to free space... Retry in 60 secs. Message reprinted in 600 secs.
2024-08-13T02:37:02.571302Z 1256492 [ERROR] [MY-000035] [Server] Disk is full writing './binlog.000556' (OS errno 28 - No space left on device). Waiting for someone to free space... Retry in 60 secs. Message reprinted in 600 secs.
2024-08-13T02:47:02.605275Z 1256492 [ERROR] [MY-000035] [Server] Disk is full writing './binlog.000556' (OS errno 28 - No space left on device). Waiting for someone to free space... Retry in 60 secs. Message reprinted in 600 secs.
2024-08-13T02:57:02.640862Z 1256492 [ERROR] [MY-000035] [Server] Disk is full writing './binlog.000556' (OS errno 28 - No space left on device). Waiting for someone to free space... Retry in 60 secs. Message reprinted in 600 secs.
2024-08-13T03:07:02.674508Z 1256492 [ERROR] [MY-000035] [Server] Disk is full writing './binlog.000556' (OS errno 28 - No space left on device). Waiting for someone to free space... Retry in 60 secs. Message reprinted in 600 secs.
2024-08-13T03:17:02.710238Z 1256492 [ERROR] [MY-000035] [Server] Disk is full writing './binlog.000556' (OS errno 28 - No space left on device). Waiting for someone to free space... Retry in 60 secs. Message reprinted in 600 secs.

除了页面实际上变快了,没有其他日志信息进一步确认

后续工作

由于日志问题导致磁盘空间不足,需要启用定时器或者自带工具进行定时清理

nacos access 日志

对于 nacos 没有提供对 access log 没有提供大小、分割配置,只有开关,而线上不建议关闭,所以需要编写 shell 脚本,加入定时器中

生产环境编写定时任务Crontab, 将脚本放到/etc/cron.daily 目录,赋予可执行权限

#!/bin/bash

logFile="/data/nacos/bin/logs/nacos_del_access.log"
# 保留14天日志
date=`date -d "$date -14 day" +"%Y-%m-%d"`
# 具体位置可调整
delFilePath="/data/nacos/bin/logs/access_log.${date}.log"

if [ ! -f "${logFile}" ];then
	echo 'access log文件打印日志频繁. /etc/cron.daily/nacosDelAccessLogs.sh 会定时删除access日志文件' >>${logFile}
fi
# 日志文件存在, 则删除
if [  -f "${delFilePath}" ];then
	rm -rf ${delFilePath}
	curDate=`date --date='0 days ago' "+%Y-%m-%d %H:%M:%S"`
	echo '['${curDate}'] 删除文件'${delFilePath} >>${logFile}
fi

MySQL binlog

建议保留七天或十四天,使用 MySQL 自带配置即可

# 我的默认配置三十天
# -- binlog_expire_logs_seconds	2592000
# SHOW VARIABLES LIKE 'binlog_expire_logs_seconds';
SET GLOBAL binlog_expire_logs_seconds = 604800;

show VARIABLES like 'expire_logs_days';
set global expire_logs_days = 7;
之前是使用这两个命令,新版 MySQL 被废弃了

永久生效,my.conf,需要重启

[mysqld]
binlog_expire_logs_seconds = 604800

后面添加日志,需要进行管理和轮换日志文件,可以考虑使用 logrotate

参考:

  1. https://stackoverflow.com/questions/58215104/whats-the-neterr-http2-protocol-error-about
  2. https://blog.csdn.net/wtzvae/article/details/107212870
  3. https://blog.51cto.com/haibo0668/5486115
  4. https://blog.csdn.net/mr_wanter/article/details/112515814
这是一段防爬代码块,我不介意文章被爬取,但请注明出处
console.log("作者主页:https://www.cnblogs.com/Go-Solo");
console.log("原文地址:https://www.cnblogs.com/Go-Solo/p/18358836");

背景

kubernetes 的原生调度器只能通过资源请求来调度 pod,这很容易造成一系列负载不均的问题,
并且很多情况下业务方都是超额申请资源,因此在原生调度器时代我们针对业务的特性以及评估等级来设置 Requests/Limit 比例来提升资源利用效率。
在这种场景下依然存在很多问题:

  1. 节点负载不均:原生 Kubernetes Scheduler 根据 Requests 和节点可分配总量来调度 Pod,既不考虑实时负载,也不估计使用量,这种纯静态的调度导致节点资源利用率分配不均。
    在流量波动性业务的场景下,在流量高峰时,部分节点利用率突破安全阈值,但是很多节点的利用率特别点,节点利用率相差特别大
  2. 业务周期性:在离线集群分离,在线集群底峰存在巨大资源浪费

本文主要讨论如果解决问题一,在线集群内部提升资源利用率

在线集群 Cpu 离散系数0.45,整个集群高峰时 Cpu 利用率仅25%左右;下图 Cpu 使用率离散图:

file

破局

基于上述情况,高峰时 Cpu 利用率仅25%肯定不是合理的情况,业界做的好的50%+。想要继续提升利用率,必须解决节点负载不均问题:

  1. 感知节点真实负载:要解决节点负载不均问题,必须要上报节点当前真实的负载
  2. 基于负载的正向调度插件:在默认调度器的基础上增加基于负载的调度插件,在正向调度是尽量保证节点间水位平均
  3. 基于负载的重调度组件:当业务不断波动,节点可能会因为应用负载变化导致节点负载出现差别,需要重调度迁移 Pod 重新达到平均

实践

关注的两个开源项目:

Koordinator:
https://koordinator.sh/

Crane:
https://gocrane.io/

相对于 Koordinator 专门为混部而生的软件,Crane以 Finops 为出发点,二者相比Koordinator更适合我们,在离线混部也是下一步计划。

调研测试

上线之后:
file

遇到的问题

  1. 热点节点问题:在业务高峰时,节点负载变高,会出现热点节点,这个时候需要重调度组件介入,把 Pod 重新调度到其他节点上

需要前置打散热点节点,这就需要对应用进行资源画像,在调度中分散这种类型的应用,避免业务高峰热点节点的产生
2. 在1中的情况下,扩容部分节点缓解集群压力时,新上的节点会迅速被热点Pod占满,导致节点负载升高,再次触发重调度

调整调度插件中负载均衡打分插件的权重,让节点负载更均衡,避免热点节点问题
3. 找到合适的节点规格,小规格节点,更容器出现热点节点

在我们的业务场景下下,当前来看48c节点热点节点出现几率小于32c

依赖倒置原则(Dependency Inversion Principle, DIP)是面向对象设计原则之一,它是SOLID原则中的"D"。依赖倒置原则的核心思想是高层策略性业务规则不应该依赖于低层的具体实现细节,而两者都应该依赖于抽象。

依赖倒置原则主要包含两个基本点:

  1. 抽象不应该依赖于细节
    :系统中的抽象层(高层模块)不应当依赖于具体实现(低层模块),而是两者都应该依赖于抽象(如接口或抽象类)。

  2. 细节应该依赖于抽象
    :具体的实现应该依赖于抽象,这样在不修改抽象层代码的情况下,可以替换或修改具体的实现。

依赖倒置原则的优点包括:

  • 降低耦合度
    :由于模块间的依赖是基于抽象的,因此减少了模块间的直接依赖,降低了耦合度。
  • 提高模块化
    :系统更容易被分解为可复用的模块,因为模块间的交互是通过抽象接口进行的。
  • 增强灵活性
    :更换或升级系统的某个部分变得更加容易,因为具体实现可以独立于高层策略进行变化。

在实际应用中,依赖倒置原则可以通过以下方式实现:

  • 使用接口或抽象类定义系统组件之间的契约。
  • 通过依赖注入(Dependency Injection, DI)将具体实现注入到需要它们的对象中,而不是让对象自己创建或查找这些实现。
  • 避免在高层模块中直接使用具体类,而是通过抽象来引用。

依赖倒置原则是实现开闭原则(Open/Closed Principle)的基础,即软件实体应该对扩展开放,对修改关闭。通过依赖倒置,我们可以更容易地扩展系统功能,而不需要修改现有的代码。

下面来看一个简单的Java代码示例,让我们更好的理解依赖倒置原则的应用:

首先,我们定义一个抽象接口,表示一个可以发送消息的系统:

public interface MessageService {
    void sendMessage(String message);
}

然后,我们创建一个具体的发送服务实现这个接口:

public class EmailService implements MessageService {
    @Override
    public void sendMessage(String message) {
        System.out.println("Sending email: " + message);
    }
}

接下来,我们有一个高层策略类,它使用
MessageService
接口而不是具体的
EmailService
类:

public class NotificationService {
    private MessageService messageService;

    // 构造函数注入依赖
    public NotificationService(MessageService messageService) {
        this.messageService = messageService;
    }

    public void notifyUser(String message) {
        // 依赖于抽象,而不是具体实现
        messageService.sendMessage(message);
    }
}

最后,我们可以在客户端代码中使用这个系统:

public class Client {
    public static void main(String[] args) {
        // 创建具体的消息服务
        MessageService emailService = new EmailService();
        
        // 将具体的消息服务注入到高层策略中
        NotificationService notificationService = new NotificationService(emailService);
        
        // 使用高层策略发送消息
        notificationService.notifyUser("Hello, this is a test email.");
    }
}

在这个例子中,
NotificationService
类是一个高层策略类,它依赖于
MessageService
接口的抽象。我们通过构造函数注入具体的消息服务
EmailService
。这样,如果将来我们需要更换消息服务的实现(比如使用
SmsService
),我们只需要创建一个新的实现类并注入到
NotificationService
中,而不需要修改
NotificationService
的代码。这就体现了依赖倒置原则的精神。

JVM 参数设置入门案例

JVM 的内存参数众多,但是在实际应用中主要关注堆内存的大小设置及堆内存中新生代和老年代的大小设置,下面看一个简单的 JVM 启动参数设置案例:

java -server
-Xms3g -Xmx3g
-XX:NewSize=1g
-XX:MetaspaceSize=128m
-XX:NewRatio=3
-XX:SurvivorRatio=8
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=dump.log -jar start.jar

-Xms -Xmx:-Xms 表示初始堆大小,-Xmx 表示最大堆大小。一般将 Xms 和 Xmx 设置为相同的值,避免垃圾回收后 JVM 重新分配堆内存大小而引起内存震荡,影响性能。可将堆内存的大小简单理解为 JVM 在运行过程中可用到的总内存大小。

-XX:NewSize:-XX:NewSize=1g 表示设置新生代的大小为 1GB,一般建议设置为总堆内存的 1/3

-XX:MetaspaceSize:表示元空间的大小为 128MB,当要加载的类库过多时,可以适当调高这个值

-XX:NewRatio:-XX:NewRatio=3 表示设置新生代与老年代的比值为 1:3,因此新生代占整个堆栈的 1/4,老年代占整个堆内存的 3/4

-XX:SurvivorRatio: -XX:SurvivorRatio=8 表示 Eden 区和两个 Survivor 区的比值为 8:1,即 Eden:SurvivorTo=8:1、Eden:SurvivorFrom=8:1。最终的结果是 Eden:SurvivorTo:SurvivorFrom=8:1:1

-XX:+UseParNewGC XX:+UseConcMarkSweepGC:垃圾回收器设置 -XX:+UseParNewGC 表示设置年轻代垃圾回收器为 ParNew 垃圾回收器。-XX:+UseConcMarkSweepGC 表示设置老年代垃圾回收器为 CMS 垃圾回收器

-OOM异常诊断设置:XX:HeapDumpOnOutOfMemoryError 表示当发生 OOM 时转储堆到文件。XX:HeapDumpPath 表示堆的转储文件路径地址。这两个参数结合起来,可以在程序出现 OOM 时及时将堆信息打印出来,方便后续分析故障


JVM 参数设置实战

在进行 JVM 参数设置时需要重点关注垃圾回收器的设置和 JVM 内存的设置。接下以在一个 8GB 的服务器上独立运行一个名为 start.jar 的 Netty 应用服务为例,介绍内存设置的流程

  1. 预留操作系统内存:首先确定操作系统的总内存为 8GB,为操作系统预留 2GB 内存,保障操作系统运行流畅,将剩余的 6GB 内存分配给应用程序
  2. 确定直接内存:由于我们的应用程序为 Netty 服务端,Netty 服务在运行过程中会使用直接内存来提高性能,因此应用程序在运行过程中会有大量直接内存的使用。为了保障应用程序既有足够的直接内存保障服务高效运行,又不至于占用过多堆外内存导致系统内存不足而产生 OOM 问题,我们将 2GB(应用程序可用内存的1/3)内存预留给直接内存,通过
    -XX:MaxDirectMemorySize-2g
    设置可用的最大堆外内存为 2GB。在使用过程中会按需分配足够的内存给直接内存,但最大不超过 2GB
  3. 确定 Java 堆的大小:剩余的 4GB 内存,将 3GB 分配给 Java 堆,这样就可以确定
    -Xm3g -Xmx3g
  4. 确定新生代和老年代的大小:由于没有特殊的大对象和过多长生命周期的对象,所以可以将堆内存的 1/3 分配给新生代,也就是 -XX:NewSize=1g,将其他剩余的 2GB 内存分配给老年代。同时,由于我们的程序为一般的 Java 程序,所以 Survivor 区和 Eden 区的配置可以采用官网建议的值,这里不做特殊设置
  5. 确定元空间区:接下来还剩余 1GB 内存可供应用程序使用,由于应用程序及其依赖的 JAR 包不大,所以可通过
    XX:MetaspaceSize=128m
    设置元空间大小为 128MB。将剩余的少部分内存留给操作系统或者其他应用程序使用
  6. 配置 GC:最后设置垃圾回收器、OOM 异常数据转储路径和 GC 日志。使用
    -XX:+UserConcMarkSweepGC
    可设置老年代使用 CMS 垃圾回收器,新生代使用默认的 ParNew 垃圾回收器。使用
    -XX:+UseG1GC
    可设置使用 G1 垃圾回收器

具体配置如下:

java -server
-XX:MaxDirectMemorySize=2g # 直接内存的大小为 2GB
-Xms3g -Xmx3g # Java 堆内存的大小为 3GB
-XX:NewSize=1g # 新生代的大小为 1GB
-XX:MetaspaceSize=128m # 元空间为 128MB
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC # 新生代使用 ParNewGC,老年代使用 CMS
-xx:+HeapDumponCutOfMemoryError # 在发生 OOM 时打印日志
-XX:HeapDumpPath=dump.log # OOM 日志存储地址
-XX:+PrintGC # 输出 GC 日志
-XX:+PrintGCDetails # 输出 GC 的详细日志
-XX:+PrintGCDatestamps # 输出 GC 的时间戳
-XX:+PrintHeapAtGC # JVM 在执行 GC 操作的前后打印堆的信息
-Xlogge:../gc/gc.log # GC日志的输出地址
-jar start.jar

另外,需要注意不同 JVM 版本的配置参数不同,比如
-XX:PermSizeXX

-XX:MaxPermsize
分别表示永久代的初始化大小和永久代的最大大小。但是在 Java8 已经没有永久代了,因此也不存在该配置参数。