2024年3月

前言

池化思想在实际开发中有很多应用,指的是针对一些创建成本高,创建频繁的对象,
用完不弃
,将其缓存在对象池子里,下次使用时优先从池子里获取,如果获取到则可以直接使用,以此降低创建对象的开销。
我们最熟悉的数据库连接池就是一种池化思想的应用,数据库操作是非常频繁的,数据库连接的创建、销毁开销很大,每次都需要进行TCP三次握手和四次挥手,权限检查等,所以如果每次操作数据库都重新创建连接,用完就丢弃,对于应用程序来说是不可接受的。在java世界里,一切皆对象,所以需要有一个数据库对象连接池,用于保存连接池对象。例如使用hikari,可以配置spring.datasource.hikari.maximum-pool-size=20,表示最多可以池化20个数据库连接对象。
此外,频繁的创建销毁对象还会影响GC,当一个对象使用完,再没被GC root引用,就变成不可达,所引用的内存可以被垃圾回收,GC是需要STW的,频繁的GC也会影响程序的吞吐量。

本篇我们要介绍的是netty的对象池
Recycler
,Recycler是对象池核心类,netty为了减少依赖,以及追求高性能,并没有使用第三方的对象池,而是自己设计了一套。
netty在高并发处理IO读写,内存对象的使用是非常频繁的,如果每次都重新申请,无疑性能会大打折扣,特别是对于堆外内存,申请和销毁的成本更高,所以对内存对象使用池化是很有必要的。
例如:PooledHeapByteBuf,PooledDirectByteBuf,ChannelOutboundBuffer.Entry都使用了对象池,这些类内部都有一个Recycler静态变量和一个Handle实例变量。

static final class Entry {
    private static final Recycler<Entry> RECYCLER = new Recycler<Entry>() {
        @Override
        protected Entry newObject(Handle<Entry> handle) {
            return new Entry(handle);
        }
    };

    private final Handle<Entry> handle;
}

原理

我们先通过一个例子感受一下Recycler的使用,然后再来分析它的原理。

public final class Connection {

	private Recycler.Handle handle;

	private Connection(Recycler.Handle handle) {
		this.handle = handle;
	}

	private static final Recycler<Connection> RECYCLER = new Recycler<Connection>() {
		@Override
		protected Connection newObject(Handle<Connection> handle) {
			return new Connection(handle);
		}
	};

	public static Connection newInstance() {
		return RECYCLER.get();
	}

	public void recycle() {
		handle.recycle(this);
	}

	public static void main(String[] args) {
		Connection c1 = Connection.newInstance();
		int hc1 = c1.hashCode();
		c1.recycle();
		Connection c2 = Connection.newInstance();
		int hc2 = c2.hashCode();
		c2.recycle();
		System.out.println(hc1 == hc2); //true
	}
}

代码非常简单,我们用final修饰Connection,这样就无法通过继承创建对象。同时构造方法定义为私有,防止外部直接new创建对象,这样就只能通过newInstance静态方法创建对象。
Recycler是一个抽象类,newObject是它的抽象方法,这里使用匿名类继承Recycler并重写newObject,用于创建一个新的对象。
Handle是一个接口,Recycler会创建并通过newObject方法传进来,默认是DefaultHandle,它的作用是用来回收对象,放回对象池。
接着我们创建两个Connection实例,可以看到它们的hashcode是一样的,证明是同一个对象。
需要注意的是,使用对象池创建的对象,用完需要调用recycle回收。

原理分析
想象一下,如果由我们设计,怎么设计一个高性能的对象池呢?对象池的操作很简单,一取一放,但考虑到多线程,实际情况就变得复杂了。
如果只有一个全局的对象池,多线程操作需要保证线程安全,那就需要通过加锁或者CAS,这都会影响存取效率,由于线程竞争,锁等待,可能通过对象池获取对象的效率还不如直接new一个,这样就得不偿失了。
针对这种情况,已经有很多的经验供我们借鉴,核心思想都是一样的,
降低锁竞争
。例如ConcurrentHashMap,通过每个节点上锁,hash到不同节点的线程就不会相互竞争;例如ThreadLocal,通过在线程级别绑定一个ThreadLocalMap,每个线程操作的都是自己的私有变量,不会相互竞争;再比如jvm在分配内存的时候,内存区域是共享的,所以jvm为每个线程设计了一块私有的TLAB,可以高效进行内存分配,关于TLAB可以参考:
这篇文章

这种无锁化的设计在netty中非常常见,例如对象池,内存分配,netty还设计了FastThreadLocal来代替jdk的ThreadLocal,使得线程内的存取更加高效。
Recycler设计如下:

如上图,Recycler内部维护了两个重要的变量,
Stack

WeakOrderQueue
,实际对象就是包装成DefaultHandle,保存在这两个结构中。
默认情况一个线程最多存储4 * 1024个对象,可以根据实际情况,通过Recycler的构造函数指定。

private static final int DEFAULT_INITIAL_MAX_CAPACITY_PER_THREAD = 4 * 1024; // Use 4k instances as default.

Stack是一个栈结构,是线程私有的,Recycler内部通过FastThreadLocal进行定义,对Stack的操作不会有线程安全问题。

 private final FastThreadLocal<Stack<T>> threadLocal = new FastThreadLocal<Stack<T>>() {};        

FastThreadLocal是netty版的ThreadLocal,搭配FastThreadLocalThread,FastThreadLocalMap使用,主要优化jdk ThreadLocal扩容需要rehash,和hash冲突问题。

当获取对象时,就是尝试从Stack栈顶pop出一个对象,如果有,则直接使用。如果没有就尝试从WeakOrderQueue“借”一点过来,放到Stack,如果借不到,那就调用newObject()创建一个。

WeakOrderQueue主要是用来解决多线程问题的,考虑这种情况,线程A创建的对象,可能被线程B使用,那么对象的释放就应该由线程B决定。如果线程B也将对象归还到线程A的Stack,那就出现了线程安全问题,线程A对Stack的读取,写入就需要加锁,影响并发效率。
为了无锁化操作,netty为其它每个线程都设计了一个WeakOrderQueue,各个线程只会操作自己的WeakOrderQueue,不会有并发问题了。其它线程的WeakOrderQueue会通过指针构成一个链表,Stack对象内部通过3个指针指向链表,这样就可以遍历整个链表对象。

站在线程A的角度,其它线程就是B,C,D...,站在线程B的角度,其它线程就是A,C,D...

从上图可以看到,WeakOrderQueue实际不是一个队列,内部是由一些Link对象构成的双向链表,它也是一个链表。
Link对象是一个包含读写索引,和一个长度为16的数组的对象,数组存储的就是DefaultHandler对象。

整个过程是这样的,当本线程从Stack获取不到可用对象时,就会通过cursor指针变量WeakOrderQueue链表,开始从其它线程获取对象。如果找到一个可用的Link,就会将整个Link里的对象迁移到Stack,然后删除链表节点,为了保证效率,每次最多迁移一个Link。如果还获取不到,就通过newObject()方法创建一个新的对象。

Recycler#get 方法如下:

 public final T get() {
    if (maxCapacityPerThread == 0) {
        return newObject((Handle<T>) NOOP_HANDLE);
    }
    Stack<T> stack = threadLocal.get();
    DefaultHandle<T> handle = stack.pop();
    if (handle == null) {
        handle = stack.newHandle();
        handle.value = newObject(handle);
    }
    return (T) handle.value;
}

pop方法判断Stack没有对象,就会调用scavenge方法,从WeakOrderQueue迁移对象。scavenge,翻译过来是拾荒,捡的意思。

 DefaultHandle<T> pop() {
    int size = this.size;
    if (size == 0) {
        if (!scavenge()) {
            return null;
        }
        size = this.size;
    }
    //...
}

最终会调用到WeakOrderQueue的transfer方法,这个方法比较复杂,主要是对WeakOrderQueue链表和内部Link链表的遍历。
这里dst就是前面说的Stack对象,可以看到会把element元素迁移过去。

boolean transfer(Stack<?> dst) {
    //...
    if (srcStart != srcEnd) {
        final DefaultHandle[] srcElems = head.elements;
        final DefaultHandle[] dstElems = dst.elements;
        int newDstSize = dstSize;
        for (int i = srcStart; i < srcEnd; i++) {
            DefaultHandle element = srcElems[i];
            if (element.recycleId == 0) {
                    element.recycleId = element.lastRecycledId;
            } else if (element.recycleId != element.lastRecycledId) {
                throw new IllegalStateException("recycled already");
            }
            srcElems[i] = null;

            if (dst.dropHandle(element)) {
                // Drop the object.
                continue;
            }
            element.stack = dst;
            dstElems[newDstSize ++] = element;
        }            
    }
    //...
}

应用

我们项目使用了mybatis plus作为orm,其中用得最多的就是QueryWrapper了,每次查询都需要new一个QueryWrapper。例如:

QueryWrapper<User> queryWrapper = new QueryWrapper();
queryWrapper.eq("uid", 123);
return userMapper.selectOne(queryWrapper);

数据库查询是非常频繁的,QueryWrapper的创建虽然不会很耗时,但过多的对象也会给GC带来压力。
QueryWrapper是mp提供的类,它没有池化的实现,不过我们可以参考上面netty DefaultHandle的思路,在它外面再包一层,然后池化包装后的对象。
回收的时候还要注意清空对象的属性,例如上面给uid赋值了123,下个对象就不能用这个条件,否则就乱套了,QueryWrapper提供了clear方法可以重置所有属性。
同时,每次用完都需要手动recycle也是比较麻烦的,开发容易忘记,可以借助AutoCloseable接口,使用try-with-resource的写法,在结束后自动完成回收。
对于修改和删除还有UpdateWrapper
和DeleteWrapper ,同样思路也可以实现。

有了这些思路,代码就出来了:

public final class WrapperUtils {

	private WrapperUtils() {}

	private static final Recycler<PooledQueryWrapper> QUERY_WRAPPER_RECYCLER = new Recycler<PooledQueryWrapper>() {
		@Override
		protected PooledQueryWrapper newObject(Handle<PooledQueryWrapper> handle) {
			return new PooledQueryWrapper<>(handle);
		}
	};

	public static <T> PooledQueryWrapper<T> newInstance() {
		return QUERY_WRAPPER_RECYCLER.get();
	}

	static class PooledQueryWrapper<T> implements AutoCloseable {

		private QueryWrapper<T> queryWrapper;
		private Recycler.Handle<PooledQueryWrapper> handle;

		public PooledQueryWrapper(Recycler.Handle<PooledQueryWrapper> handle) {
			this.queryWrapper = new QueryWrapper<>();
			this.handle = handle;
		}

		public QueryWrapper<T> getWrapper() {
			return this.queryWrapper;
		}

		@Override
		public void close() {
			queryWrapper.clear();
			handle.recycle(this);
		}
	}
}

使用如下,可以看到打印出来的hashcode都是一样的,每次执行后都会自动调用close方法,进行QueryWrapper属性重置。

public static void main(String[] args) {
	try (PooledQueryWrapper<Case> objectPooledWrapper = WrapperUtils.newInstance()) {
		QueryWrapper<Case> wrapper = objectPooledWrapper.getWrapper();
		wrapper.eq("age", 1);
		wrapper.select("id,name");
		wrapper.last("limit 1");
		System.out.println(wrapper.hashCode());
	}

	try (PooledQueryWrapper<Case> objectPooledWrapper = WrapperUtils.newInstance()) {
		QueryWrapper<Case> wrapper = objectPooledWrapper.getWrapper();
		wrapper.eq("age", 2);
		wrapper.select("id,email");
		wrapper.last("limit 2");
		System.out.println(wrapper.hashCode());
	}

	try (PooledQueryWrapper<Case> objectPooledWrapper = WrapperUtils.newInstance()) {
		QueryWrapper<Case> wrapper = objectPooledWrapper.getWrapper();
		wrapper.eq("age", 3);
		wrapper.select("id,phone");
		wrapper.last("limit 3");
		System.out.println(wrapper.hashCode());
	}
}

总结

之前我们也分析过
apache common pool
,这也是一个池化实现,在redis客户端也有应用,但它是通过加锁解决并发问题的,设计没有netty这么精细。
上面的源码来自netty4.1.42,从整体上看整个Recycler的设计还是比较复杂的,主要为了解决多线程竞争和GC问题,导致整个代码复杂度比较高,所以netty在后来的版本中对其进行重构。
不过这不影响我们对它思想的学习,以后也可以借鉴到实际开发中。

更多分享,欢迎关注我的github:
https://github.com/jmilktea/jtea

Kubernetes 彻底改变了容器编排,简化了应用程序的管理和扩展。然而,与任何复杂系统一样,Kubernetes 集群也会遇到问题,需要及时解决才能保持最佳性能和可靠性。
在本文中,我们将深入探讨必要的
kubectl
命令,这些命令是诊断和排除 Kubernetes 集群问题不可或缺的工具

。无论您是新手还是经验丰富的 Kubernetes 用户,掌握这些命令都将使您有能力驾驭错综复杂的容器编排,确保应用程序的健康。

查看集群记录报告

排除 Kubernetes 集群故障的第一步是检查其中发生的事件。
kubectl get events --all-namespaces
命令能全面查看所有命名空间的事件,让您发现与 pod、节点和其他资源相关的错误、警告和问题。

NAMESPACE   LAST SEEN   TYPE      REASON              OBJECT                   MESSAGE
default     5m          Normal    Scheduled           pod/my-pod               Successfully assigned default/my-pod to node-1
default     4m          Normal    Pulling             pod/my-pod               Pulling image "my-image:latest"
default     4m          Normal    Pulled              pod/my-pod               Successfully pulled image "my-image:latest"
default     4m          Normal    Created             pod/my-pod               Created container my-container
default     4m          Normal    Started             pod/my-pod               Started container my-container
kube-system 15m         Normal    RegisteredNode      node/node-1              Node node-1 event: Registered Node node-1 in Controller
...

下面是输出结果中各列的细目:

  • NAMESPACE:事件发生的命名空间。
  • LAST SEEN:事件最后一次出现的时间。
  • TYPE:类型:事件类型(如 Normal 或 Warning)。
  • REASON:事件发生的原因。
  • OBJECT:与事件相关的 Kubernetes 资源(如 pod、节点)。
  • MESSAGE:与事件相关的描述或消息。

排除 pod 初始化故障

假设您遇到了 pod 无法正确初始化的问题。您可以使用
kubectl get events --all-namespaces
来识别与 pod 初始化失败相关的事件,帮助您找出根本原因。

检查 pod 日志

当出现应用程序级问题时,检查 pod 日志至关重要。使用
kubectl logs <pod-name> -n <namespace>
查看给定命名空间中特定 pod 的日志。该命令对于识别应用程序代码中的错误、异常或问题非常有用。

kubectl logs my-pod -n my-namespace

在这个例子中:

  • my-pod
    是要从中获取日志的 pod 的名称。
  • my-namespace
    是 pod 所在的命名空间。

调试应用程序错误

想象一下,在
my-namespace
中名为
my-pod
的 pod 中运行着一个应用程序。如果应用程序报错,您可以使用
kubectl logs
检索特定的错误信息,从而帮助调试和解决问题。

描述资源

kubectl describe
命令提供有关各种 Kubernetes 资源(如 pod、节点和部署)的详细信息。通过运行
kubectl describe <resource> <resource-name> -n <namespace>
,您可以访问大量数据,包括事件、条件和配置详情,帮助您找出问题的根源。

kubectl describe pod my-pod -n my-namespace

在这个例子中:

  • pod
    是资源类型。
  • my-pod
    是要描述的特定 pod 的名称。
  • my-namespace
    是 pod 所在的命名空间。

运行此命令后,将显示指定 pod 的详细信息。输出将包括有关 pod 的元数据、条件、事件等信息的不同部分。下面是输出结果的示例:

Name:         my-pod
Namespace:    my-namespace
...
Containers:
  my-container:
    Container ID:   container-id
    Image:          my-image:latest
    ...
Conditions:
  Type              Status
  ----              ------
  Initialized       True
  Ready             True
  ContainersReady   True
  PodScheduled      True
...
Events:
  Type     Reason       Age                 From               Message
  ----     ------       ----                ----               -------
  Normal   Scheduled    3m                  default-scheduler  Successfully assigned my-namespace/my-pod to node-1
  Normal   Pulled       2m                  kubelet, node-1    Container image "my-image:latest" already present on machine
  Normal   Created      2m                  kubelet, node-1    Created container my-container
  Normal   Started      2m                  kubelet, node-1    Started container my-container
...

输出将包含
指定 pod 的详细信息,包括其元数据、容器信息、条件和事件
。这些信息对于排除 pod 的故障会非常有用,比如初始化问题、就绪问题或与其生命周期相关的事件。

获取 pod 信息

命令:

kubectl get pods -n <namespace>

说明

kubectl get pods
命令用于检索 Kubernetes 集群中运行的 pod 的信息。

-n <namespace>
指定要列出 pod 的命名空间。请将
<namespace>
替换为您感兴趣的实际命名空间名称。

示例

假设您有一个包含多个命名空间的 Kubernetes 集群,您想检查 my-namespace 命名空间中 pod 的状态。您可以使用以下命令:

kubectl get pods -n my-namespace

运行该命令时,输出结果可能如下:

NAME        READY   STATUS      RESTARTS   AGE
app-pod-1   1/1     Running     0          2d
app-pod-2   1/1     Running     0          1d
app-pod-3   0/1     Pending     0          1h
app-pod-4   1/1     Running     0          30m

检查节点状态

节点是 Kubernetes 集群的支柱。为确保一切运行顺利,可执行
kubectl get nodes
查看所有节点的状态。

kubectl get nodes

运行此命令后,您将看到集群中所有节点的列表及其当前状态。状态可以是以下其中之一:

  • Ready
    :这是理想状态。这意味着节点是健康的,可以接受和运行容器。
  • NotReady
    :这种状态表示节点无法正常运行,或者遇到了妨碍其运行容器的问题。处于这种状态的节点可能存在资源限制、网络问题或其他问题。
  • SchedulingDisabled
    :这种状态意味着节点被明确标记为不可调度,从而无法在其上调度新容器。这对于维护或故障排除很有用。
  • Unknown
    :在某些情况下,由于与 Kubernetes 控制平面的通信问题,节点的状态可能是未知的。

理想情况下,
在一个健康的 Kubernetes 集群中,所有节点都应处于 "Ready"状态

示例

假设您运行
kubectl get nodes
并看到以下输出:

NAME       STATUS     ROLES    AGE   VERSION
node-1     Ready      <none>   30d   v1.21.3
node-2     Ready      <none>   30d   v1.21.3
node-3     NotReady   <none>   5m    v1.21.3

在容器中执行

有时,调试需要亲自动手。通过
kubectl exec
,您可以使用
/bin/bash
以交互方式进入容器。当您需要在容器内调查问题时,这一点尤其有用。

示例

假设在命名空间
my-namespace
中有一个名为
my-pod
的 Kubernetes pod。在这个 pod 中,有一个名为
my-container
的容器。您怀疑这个容器中存在问题,并想进行交互式调查。

下面是使用
kubectl exec
的方法:

kubectl exec -it my-pod -n my-namespace -- /bin/bash

在该命令中:

  • -it
    用于指定交互式终端,允许您与容器内的 shell 进行交互。
  • my-pod
    是要访问的 pod 的名称。
  • -n my-namespace
    指定 pod 所在的命名空间。
  • -- /bin/bash
    指定要在容器内运行的命令。在本例中,它是
    /bin/bash
    ,用于启动 Bash shell。

运行命令后,您将进入容器的 shell。现在,您可以像登录到容器的操作系统一样,交互式地运行命令、检查文件、查看日志和排除故障。

下面是一个会话的案例:

root@my-pod:/app# ls
file1.txt  file2.txt  file3.txt
root@my-pod:/app# cat file1.txt
Contents of file1.txt
root@my-pod:/app# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0   1234   567 ?        Ss   Sep01   0:01 /my-app
...
root@my-pod:/app# exit

完成调试或故障排除任务后,可以键入
exit
退出容器的 shell。这将返回本地终端。

使用
kubectl exec

/bin/bash
是诊断和解决容器内问题的有力工具,在传统调试方法不充分的情况下尤为重要。

用于服务调试的端口转发

要调试 pod 内运行的服务,可以使用
kubectl port-forward
将本地端口转发到 pod 上的端口。这样就能像访问本地运行的服务一样访问这些服务,从而方便调试和测试。

示例

假设您在命名空间
my-namespace
中有一个名为
my-app-pod
的 Kubernetes pod,它在 8080 端口上托管了一个服务,您想访问该服务进行调试。

下面是使用
kubectl port-forward
将本地端口转发到 pod 端口的方法:

kubectl port-forward my-app-pod -n my-namespace 8080:8080

在该命令中:

  • my-app-pod
    是要将流量转发到的 pod 的名称。
  • -n my-namespace
    指定 pod 所在的名称空间。
  • 8080:8080
    表示要将流量从本地计算机的 8080 端口转发到 pod 的 8080 端口。

建立端口转发后,您就可以通过连接本地计算机上的
http://localhost:8080
访问 pod 内运行的服务。向本地 8080 端口发出的任何请求都将被转发到 pod 上的相应端口。下面是一个案例:

Forwarding from 127.0.0.1:8080 -> 8080
Handling connection for 8080

现在,您可以打开网络浏览器,或使用
curl

wget
等工具访问 pod 中的服务,就像它在本地运行一样:

curl http://localhost:8080

这样,您就可以直接检查服务并与之交互、测试 API 端点或调试问题,而无需将服务暴露在更广泛的网络中。完成端口转发后,可以在终端按下
Ctrl+C
停止端口转发。

使用
kubectl port-forward
是调试和测试 Kubernetes pod 中运行的服务的便捷方法,无需复杂的网络配置或将服务暴露在外部。

资源指标

kubectl top nodes

kubectl top pods -n <namespace>
可实时了解 CPU 和内存的使用情况,帮助优化资源。

示例

假设您想监控 Kubernetes 集群中节点和 pod 的 CPU 和内存使用情况。

节点指标

要查看节点的资源使用指标,可以使用以下命令:

kubectl top nodes

下面是输出结果的例子:

NAME       CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
node-1     100m         5%     1234Mi          40%
node-2     80m          4%     987Mi           32%

在该输出中:

  • CPU(cores)
    显示以毫核为单位的 CPU 使用率。
  • CPU%
    表示 CPU 使用率的百分比。
  • MEMORY(bytes)
    以兆字节为单位显示内存使用情况。
  • MEMORY%
    表示内存使用百分比。

您可以快速识别 CPU 或内存负载较高的节点,并采取相应措施,如缩放或调整资源分配。

Pod 指标

要查看特定命名空间中 pod 的资源使用指标,请使用以下命令:

kubectl top pods -n my-namespace


my-namespace
替换为运行 pod 的实际名称空间。下面是 pod 指标输出的示例:

NAME          CPU(cores)   MEMORY(bytes)
my-pod-1      50m          128Mi
my-pod-2      30m          64Mi
my-pod-3      70m          256Mi

在该输出中:

  • CPU(cores)
    显示 pod 的 CPU 使用情况,单位为毫核。
  • MEMORY(bytes)
    以字节为单位显示 pod 的内存使用情况。

通过监控 pod 指标,您可以识别资源密集型 pod,优化资源分配,并就 pod 的扩展或资源请求和限制做出明智的决策。

kubectl top
命令提供的资源使用指标对于确保 Kubernetes 集群中资源的有效使用、识别性能瓶颈以及优化应用程序的资源分配都很有价值。

检查 API 服务器健康状况

Kubernetes API 服务器对集群运行至关重要。使用
kubectl get --raw=/healthz
查询其健康状况端点,可以快速评估其健康状况。

在该输出中:

 kubectl get --raw=/healthz
 ok

响应 “ok” 表示 API 服务器正常运行。

如果 API 服务器不健康或出现问题,您可能会收到错误信息或非 “ok” 响应,这可能表明存在需要调查和解决的问题。

验证版本

最后,确保 Kubernetes 客户端和服务器版本之间的兼容性至关重要。使用
kubectl version
获取两个版本的信息。

示例

要检查客户端和服务器版本,只需运行:

kubectl version

输出结果:

Client Version: version.Info{Major:"1", Minor:"21", GitVersion:"v1.21.3", ...
Server Version: version.Info{Major:"1", Minor:"21", GitVersion:"v1.21.3", ...

在该输出中:

  • Client Version
    提供了本地
    kubectl
    客户端的相关信息。其中包括主版本、次版本和 GitVersion,后者指定了客户端的确切版本。
  • Server Version
    提供
    kubectl
    客户端所连接的 Kubernetes API 服务器的信息。它包括服务器的主版本、次版本和 GitVersion。

确保客户端版本与服务器版本匹配或兼容。主要版本差异可能表示不兼容,应加以解决。一般建议客户端和服务器版本尽量保持一致,以避免出现兼容性问题。

检查版本与
kubectl version
的兼容性是确保 Kubernetes 环境稳定、运行良好的必要步骤。

结论

掌握 Kubernetes 故障排除是任何 Kubernetes 管理员或开发人员的一项重要技能。本文概述的
kubectl
命令是您诊断和解决集群问题的盟友。请记住,有效的故障排除通常需要结合使用这些命令以及对应用程序和基础架构的深入了解。此外,考虑实施 Prometheus 和 Grafana 等监控和可观察性解决方案,以主动发现和解决问题,确保 Kubernetes 集群的稳定性和性能。通过利用这些工具和技术,您可以自信地驾驭复杂的 Kubernetes 世界,并保持应用程序的平稳运行。

image

我们都知道,我们写的Java程序需要先经过编译,生成了.class文件(字节码文件)。然而,计算机并不能直接解释.class文件里面的内容,这时候就需要一个能加载、解释.class文件并且能按.class文件里的内容进行处理的一个东西--JVM。

JVM,就是Java虚拟机。它是一种规范,有针对不同系统的特定实现(Linux,Windows,macOS)。这样,相同的字节码就能在不同的系统上运行,实现了跨平台运行(Write Once, Run Anywhere)。

JVM的内存结构

JVM的内存结构

上图是JDK1.8的JVM内存结构,可以看出内存结构分为程序计数器、Java虚拟机栈、本地方法栈、堆、元空间,其中程序计数器、Java虚拟机栈、本地方法栈是线程独享的(按线程隔离),其生命周期和所在线程相同,而堆、元空间是线程共享的。

程序计数器

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。

注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

Java虚拟机栈

每个Java方法在执行时都会创建一个栈帧(Java方法执行的内存模型)。每一个方法从被调用到执行完成的过程,就是一个栈帧在Java虚拟机栈中入栈到出栈的过程。栈是先进后出的数据结构,也就是说,后被调用的Java方法会先结束。

Java虚拟机栈

上图就是一个Java虚拟机栈的结构,一个Java虚拟机栈是由一个个栈帧组成的,而每个栈帧中都拥有局部变量表、操作数栈、动态链接、方法返回地址。

Java虚拟机栈可能会出现以下两种错误:

StackOverFlowError:若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError错误。

OutOfMemoryError:如果栈的内存大小可以动态扩展,如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

局部变量表
主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。

操作数栈
主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。

动态链接
主要服务一个方法需要调用其他方法的场景。在 Java 源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用(Symbilic Reference)保存在 Class 文件的常量池里。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的内存地址的直接引用。

动态链接

本地方法栈

本地方法栈与Java虚拟机栈作用相似,它们之间的区别是Java虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、方法返回地址。

堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。堆的作用就是存放对象实例,几乎所有对象实例都在这个区域分配内存。

Java堆是垃圾收集器管理的主要区域,因此也叫GC堆(Garbage Collected Heap)。从内存回收的角度(收集器一般采用分代收集算法),Java堆内存可以细分为:新生代和老年代。新生代再细分有:Eden区、Survivor0区、Survivor1区。

堆

根据虚拟机规范,Java堆可以处于物理上的不连续内存中,只要逻辑上是连续即可。其大小可以通过-Xmx和-Xms控制。如果在堆中没有内存完成实例分配,并且堆也无法扩展时会抛出OutOfMemoryError异常。

方法区

方法区结构

虚拟机要使用一个类时,它需要读取并解析.class文件获取相关信息,再将信息存入到方法区。方法区用于存放类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

《Java 虚拟机规范》只是定义了方法区这个概念和它的作用,在不同的虚拟机实现上,方法区的实现是不同的。JDK1.8之前的方法区实现叫永久代,到了JDK1.8,方法区实现叫元空间,它取代了永久代。元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。

方法区、永久代、元空间三者关系

元空间 (MetaSpace)替代永久代(PermGen) 的原因如下:
1、永久代受到JVM本身设置的固定大小限制,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比永久代出现的几率会更小。
2、永久代的对象是通过FullGC进行垃圾收集,也就是和老年代同时实现垃圾收集。替换成元空间以后,简化了Full GC。可以在不进行暂停的情况下并发地释放类数据,同时也提升了GC的性能。
3、在JDK1.8,合并HotSpot和JRockit的代码时, JRockit从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了。

前言

异常的处理在我们应用程序中是至关重要的,在
dotNet
中有很多异常处理的机制,比如
MVC的异常筛选器
, 管道中间件定义
try catch
捕获异常处理亦或者第三方的解决方案
Hellang.Middleware.ProblemDetails
等。
MVC异常筛选器
不太灵活,对管道的部分异常捕获不到,后两种方式大家项目应该经常出现。


dotNet8
发布之后支持了新的异常处理机制
IExceptionHandler
或者
UseExceptionHandler
异常处理程序的
lambda
配置,配合
dotNet7
原生支持的
ProblemDetail
使得异常处理更加规范。

本文用一个简单的
Demo
带大家看一下新的异常处理方式

文末有示例完整的源代码

先起一个
WebApi
的新项目

Problem Details

Problem Details
是一种在
HTTP API
中用于描述错误信息的标准化格式。根据
RFC 7807
,Problem Details 提供了一种统一、可机器读取的方式来呈现出发生在 API 请求中的问题。它包括各种属性,如 title、status、detail、type 等,用于清晰地描述错误的性质和原因。通过使用 Problem Details,开发人员可以为 API 的错误响应提供一致性和易于理解的结构化格式,从而帮助客户端更好地处理和解决问题。

项目中使用 Problem Details

builder.Services.AddProblemDetails();

如果我们不对异常进行捕获处理,
Asp.Net Core
提供了两种不同的内置集中式机制来处理未经处理的异常

  • app.UseDeveloperExceptionPage();

    开发人员异常中间件


    开发人员异常中间件
    会显示服务器错误的详细堆栈跟踪,不建议在非开发环境显示,暴漏核心错误信息给客户端,有严重的安全风险

  • app.UseExceptionHandler(); 异常处理程序中间件,
    使用异常处理程序中间件生成的是标准的简化回复

测试 UseDeveloperExceptionPage

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddProblemDetails();
var app = builder.Build();
app.MapGet("/TestUseDeveloperExceptionPage",
    () => { throw new Exception("测试UseDeveloperExceptionPage"); });
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
    app.UseDeveloperExceptionPage();// 开发人员异常页
}
app.UseStatusCodePages();
app.UseHttpsRedirection();
app.Run();

调用
TestUseDeveloperExceptionPage
接口
回参

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.6.1",
  "title": "System.Exception",
  "status": 500,
  "detail": "测试UseDeveloperExceptionPage",
  "exception": {
    "details": "System.Exception: 测试UseDeveloperExceptionPage\r\n   at Program.<>c.<<Main>$>b__0_0() in C:\\dotNetParadise\\dot-net-paradise-exception\\dotNetParadise-Exception\\dotNetParadise-Exception\\Program.cs:line 7\r\n   at lambda_method3(Closure, Object, HttpContext)\r\n   at Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware.Invoke(HttpContext context)\r\n   at Microsoft.AspNetCore.Diagnostics.StatusCodePagesMiddleware.Invoke(HttpContext context)\r\n   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)",
    "headers": {
      "Accept": [
        "*/*"
      ],
      "Host": [
        "localhost:7130"
      ],
      "User-Agent": [
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0"
      ],
      "Accept-Encoding": [
        "gzip, deflate, br"
      ],
      "Accept-Language": [
        "en-US,en;q=0.9"
      ],
      "Cookie": [
        "ajs_anonymous_id=b96604ea-c096-4693-acfb-b3a9e8403f0e; Quasar_admin_Vue3_username=admin; Quasar_admin_Vue3_token=b1aa15b6-02bb-44b9-8668-0157a1d9b6f0; Quasar_admin_Vue3_lang=en-US"
      ],
      "Referer": [
        "https://localhost:7130/swagger/index.html"
      ],
      "sec-ch-ua": [
        "\"Chromium\";v=\"122\", \"Not(A:Brand\";v=\"24\", \"Microsoft Edge\";v=\"122\""
      ],
      "sec-ch-ua-mobile": [
        "?0"
      ],
      "sec-ch-ua-platform": [
        "\"Windows\""
      ],
      "sec-fetch-site": [
        "same-origin"
      ],
      "sec-fetch-mode": [
        "cors"
      ],
      "sec-fetch-dest": [
        "empty"
      ]
    },
    "path": "/TestUseDeveloperExceptionPage",
    "endpoint": "HTTP: GET /TestUseDeveloperExceptionPage",
    "routeValues": {}
  }

可以看到所有的信息都抛出来给到了客户端,适合在开发环境用,非开发环境尤其是生产环境不要启用。

app.UseExceptionHandler();

异常处理程序中间件

// app.UseDeveloperExceptionPage();// 开发人员异常页
app.UseExceptionHandler();//异常处理中间件

测试一下

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.6.1",
  "title": "An error occurred while processing your request.",
  "status": 500
}

可以看到只保留了最基本的报错信息,这样第一步我们已经完成了。

自定义异常 和 IExceptionHandler

创建一个自定义异常信息

public class CustomException(int code, string message) : Exception(message)
{
    public int Code { get; private set; } = code;

    public string Message { get; private set; } = message;
}

集成
IExceptionHandler
创建自定义异常处理器

public class CustomExceptionHandler(ILogger<CustomException> logger, IWebHostEnvironment environment) : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
    {
        if (exception is not CustomException customException) return false;
        logger.LogError(
              exception, "Exception occurred: {Message} {StackTrace} {Source}", exception.Message, exception.StackTrace, exception.Source);

        var problemDetails = new ProblemDetails
        {
            Status = customException.Code,
            Title = customException.Message,
        };
        if (environment.IsDevelopment())
        {
            problemDetails.Detail = $"Exception occurred: {customException.Message} {customException.StackTrace} {customException.Source}";
        }
        httpContext.Response.StatusCode = problemDetails.Status.Value;
        await httpContext.Response
            .WriteAsJsonAsync(problemDetails, cancellationToken);
        return true;
    }
}

可以注册多个自定义异常处理器分别处理不同类型的异常,按默认的注册顺序来处理,如果返回
true
则会处理此异常返回
false
会跳到下一个
ExceptionHandler
,没处理的异常在 UseExceptionHandler 中间件做最后处理。

创建第二个
ExceptionHandler
处理系统异常

public class SystemExceptionHandle(ILogger<CustomException> logger, IWebHostEnvironment environment) : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
    {
        if (exception is CustomException) return false;
        logger.LogError(
              exception, "Exception occurred: {Message} {StackTrace} {Source}", exception.Message, exception.StackTrace, exception.Source);

        var problemDetails = new ProblemDetails
        {
            Status = StatusCodes.Status500InternalServerError,
            Title = "An error occurred while processing your request",
        };
        if (environment.IsDevelopment())
        {
            problemDetails.Detail = $"Exception occurred: {exception.Message} {exception.StackTrace} {exception.Source}";
        }

        httpContext.Response.StatusCode = problemDetails.Status.Value;

        await httpContext.Response
            .WriteAsJsonAsync(problemDetails, cancellationToken);

        return true;

    }
}

IOC 容器注册
ExceptionHandler

builder.Services.AddExceptionHandler<CustomExceptionHandler>();
builder.Services.AddExceptionHandler<SystemExceptionHandle>();

新加接口测试一下

app.MapGet("/CustomThrow", () =>
{
    throw new CustomException(StatusCodes.Status403Forbidden, "你没有权限!");
}).WithOpenApi();

回参

{
  "title": "你没有权限!",
  "status": 403,
  "detail": "Exception occurred: 你没有权限!    at Program.<>c.<<Main>$>b__0_1() in C:\\dotNetParadise\\dot-net-paradise-exception\\dotNetParadise-Exception\\dotNetParadise-Exception\\Program.cs:line 15\r\n   at lambda_method5(Closure, Object, HttpContext)\r\n   at Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware.Invoke(HttpContext context)\r\n   at Microsoft.AspNetCore.Diagnostics.StatusCodePagesMiddleware.Invoke(HttpContext context)\r\n   at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddlewareImpl.<Invoke>g__Awaited|10_0(ExceptionHandlerMiddlewareImpl middleware, HttpContext context, Task task) dotNetParadise-Exception"
}

可以看出全局异常捕获生效了。

最后

本文讲的是 dotNet8 新的异常处理方式,当时也可以用
UseExceptionHandler

lambda
方式可以创建,但是不如这种强类型约束的规范,大家在升级 dotNet8 时可以参考本文来修改项目现有的全部异常捕获方式。

Demo 源代码

dotNet 官网教程

这几天无聊,想到原来的壁纸管理器应用能够实现成插件的形式,然后思考了一下,打算把原来壁纸管理器的代码用插件形式来进行实现,于是经过几天的努力,终于完成了插件版的壁纸管理器。以前有写过C#的插件的例子(
https://www.cnblogs.com/lzhdim/p/17023591.html
)。此博文进行介绍该插件版内容:

1、
项目目录;

上面是插件程序列表,下面是此应用的项目。

2、
源码介绍;

1) 主窗体插件代码;

2) 插件接口实现代码;

3、
运行界面;

1) 主窗体;

比原来单例版的窗口标题栏进行了改动。

2) 关于窗体;

关于窗体也进行了改动,加入了显示插件列表内容。

4、
使用介绍;

此壁纸管理器主要由主窗体和插件窗体组成,在主窗体使用反射获取插件信息并加载,插件窗体用于实现功能。具体看源码介绍里的内容。

5、
源码下载;

源码下载地址:
https://download.csdn.net/download/lzhdim/88965765

6、
其它建议;

这个应用是一个使用插件开发的例子,具体请读者自己修改插件接口定义及修改插件里窗体功能。

此文介绍的一个例子,作为原来壁纸管理器的插件版,比原来的源码有一定的改进和优化完善,需要参考的读者请自己翻看源码进行代码复用。