wenmo8 发布的文章

CameraLink协议介绍

CameraLink是一种用于机器视觉和工业成像应用的标准化数字接口协议。它由自动化成像协会(Automated Imaging Association)开发,旨在解决传统模拟视频接口的局限性,提供一种高效、可靠且易于使用的数字解决方案,以实现相机与图像处理系统之间的高速数据传输。

CameraLink优势

高清视频:
CameraLink协议支持高分辨率、高帧率视频传输,能够满足捕捉快速运动或进行高速图像采集等应用场景。

稳定可靠:
CameraLink采用低压差分信号(LVDS)传输技术,能够有效抵抗电磁干扰和噪音,确保了信号在传输过程中的稳定性和抗干扰能力。

灵活配置:
CameraLink协议提供了Base、Medium、Full等多种配置选项,以适应不同带宽需求的应用场景。

图 1

12G-SDI介绍

SDI(Serial Digital Interface,串行数字接口)是一种用于传输未经压缩的数字视频信号的标准,主要应用于远程监控、工业检测等领域。12G-SDI是SDI接口的其中一种,支持高达12Gbps的数据传输速率,专为支持4K超高清视频而设计。

12G-SDI优势

支持单链路传输:
12G-SDI能够在单根电缆上传输,简化安装与维护,减少布线复杂性。

支持长距离传输:
12G-SDI支持长距离传输,能够实现远距离的高质量视频信号传输。

支持4K高清视频:
12G-SDI支持4K分辨率,帧率高达60fps,能够流畅地播放动态视频。

图 2

RK3588J+FPGA国产平台

瑞芯微RK3588J/RK3588处理器集成了四核2.4GHz ARM Cortex-A76与四核1.8GHz ARM Cortex-A55。创龙科技基于瑞芯微RK3588J/RK3588 + 紫光同创Titan-2 PG2T390H(兼容Xilinx Kintex-7 XC7K325T)FPGA,推出了SOM-TL3588F工业核心板和TL3588F-EVM评估板。

创龙的SOM-TL3588F核心板的ARM、FPGA、ROM、RAM、电源、晶振、连接器等所有元器件均采用国产工业级方案,国产化率100%。此外,RK3588J + FPGA评估板具备丰富的接口资源,包括Ethernet、RS422/RS485、USB 3.1、CAN、SFP+等通信接口,以及MIPI CSI、CameraLink Base、HDMI、12G-SDI等音视频接口,满足客户的项目评估需求!

RK3588J+FPGA核心板优势

接口拓展灵活便捷

2路CameraLink Base,支持Full模式

12G-SDI IN/OUT接口,支持4K@60fps高清视频,由高速串行收发器HSST引出

强大编解码能力

8K@60fps H.265、8K@30fps H.264视频解码

8K@30fps H.265/H.264视频编码

6T超强算力NPU

支持INT4/INT8/INT16/FP16/BF16/TF32

支持TensorFlow/PyTorch/Caffe/MXNet深度学习框架

图 3

RK3588J+FPGA核心板典型应用领域

图 4

12G-SDI与CameraLink输入输出方案演示

本文主要介绍基于瑞芯微RK3588J + FPGA的高清视频输入案例,适用开发环境如下。

Windows开发环境:Windows 7 64bit、Windows 10 64bit

FPGA端开发环境:Xilinx Vivado 2017.4、Xilinx SDK 2017.4

硬件平台:TL3588F-EVM(基于RK3588J + Kintex-7)

为了简化描述,本文仅摘录部分方案功能描述与测试结果。

cameralink_display案例

案例说明

案例通过TL3588F-EVM评估板的CameraLink接口进行分辨率为1280x1024、最高帧率为289.41fps的视频采集,并通过TLCameraLinkF模块的HDMI接口输出采集到的视频,分辨率为1920X1080、帧率为60fps。

案例演示

请将TLCameraLinkF模块、CameraLink相机、HDMI显示屏、评估板等对应连接,硬件连接如下图所示。

图 5

运行Full模式程序,即可看到串口终端打印如下信息。输入"1"选择相机型号为RS-A5241-CM107-S00,再输入"1"选择为Full模式。配置完成后,即可看到HDMI显示屏输出黑白图像。

图 6
图 7

gtx_sdi_cap_dis案例

案例说明

TL3588F-EVM评估板通过12G-SDI IN接口进行1080P@60fps的视频采集,并通过评估板的12G-SDI OUT接口将采集到的视频进行输出。

案例演示

将PC机HDMI OUT接口、HDMI转SDI模块、HDMI转SDI模块、HDMI显示屏、评估板等对应连接,硬件连接如下图所示。

图 8

运行程序,即可看到串口终端打印如下信息,然后在PC机的显卡设置(或图形属性)中,按照下图设置HDMI分辨率为1920x1080、刷新率为60p Hz。

图 9
图 10

此时,HDMI显示屏将显示PC机HDMI OUT接口输出的图像。

图 11

到这里,我们的演示步骤结束。想要查看更多瑞芯微RK3588J + FPGA国产平台更多相关的案例演示,欢迎各位工程师通过公众号(Tronlong创龙科技)查看,快来试试吧!


Manim
中,其实
直线移动
的动画非常简单,每个
Mobject
对象都有
animate
属性,

通过
obj.animate.shift()
或者
obj.animate.move_to()
很容易将对象从一个位置移往另一个位置。

不过,如果要更复杂的移动路线,那么
animate
属性的移动方法就无法满足了。

本篇介绍
Manim
中的两个处理复杂移动动画的类
MoveAlongPath

PhaseFlow

MoveAlongPath
能使图形对象沿着指定路径(如贝塞尔曲线等)移动,并且可精细调节运动参数。

而且它易于和其他动画组合,主要用于动画制作、路径演示和物理轨迹模拟等场景的动画类。

PhaseFlow
是一个基于向量场和微分方程,通过数值计算求解轨迹的移动动画。

它可以展示相空间中系统状态动态演化过程,多用在物理系统模拟、控制系统分析和生物系统建模等领域。

1. 动画概述

1.1. MoveAlongPath

MoveAlongPath

核心特点
是能让一个 Mobject(manim 中的图形对象)沿着指定的路径进行移动。

这个路径可以是通过各种方式定义的,比如使用贝塞尔曲线、折线等。

比如,你可以定义一个复杂的二次贝塞尔曲线作为路径,然后让一个圆形 Mobject 沿着这条曲线运动。

此外,它还允许你对运动的参数进行精细控制,可以设置运动的
速度
,通过改变速度参数来实现匀速运动或者变速运动。

比如,你可以让一个对象开始运动较慢,然后逐渐加速沿着路径前进,还可以指定运动的起始点和结束点在路径上的位置,这样就能灵活地控制对象在路径的哪一段进行运动。

最后,
MoveAlongPath
可以很方便地与其他动画效果组合使用。

比如,在对象沿着路径运动的同时,可以结合旋转动画,让对象在移动过程中自身还进行旋转,从而创造出更加复杂和生动的动画效果。

它的参数主要有:

参数名称 类型 说明
mobject Mobject 要移动的
Mobject
对象
path VMobject 指定移动的路径
suspend_mobject_updating bool 控制是否暂停
mobject
的更新

suspend_mobject_updating
参数如果为
True
,则在动画插值过程中暂停
mobject
的更新;

如果为
False
,则继续更新。

1.2. PhaseFlow

PhaseFlow
是和动力系统紧密相关的动画效果类。

它是基于向量场来工作的,这个向量场通常是由一个或多个微分方程定义的。

比如,对于一个简单的二维动力系统,可能有$ \frac{dx}{dt}=f(x,y)
\(和\)
\frac{dy}{dt}=g(x,y) $这样的微分方程组来定义向量场,
PhaseFlow
会根据这些方程计算相空间中的轨迹。

PhaseFlow
主要用于展示相空间(
Phase Space
)中系统状态的动态演化过程。

相空间
是一个抽象的空间,其坐标轴可以代表系统的状态变量(如位置、速度等)。

PhaseFlow
能够以动画的形式展示从不同初始状态出发的轨迹是如何在相空间中随着时间流动的,并且这种展示是连续而平滑的。

它的参数主要有:

参数名称 类型 说明
mobject Mobject 要移动的
Mobject
对象
function func 定义相流的动态行为
virtual_time float 虚拟时间,用于在计算相流轨迹时确定时间步长等相关计算
suspend_mobject_updating bool 控制是否暂停
mobject
的更新
rate_func func 控制动画的速率

function
参数通常基于给定的微分方程来计算相空间中的轨迹变化。

2. 使用示例

下面通过示例来演示
MoveAlongPath

PhaseFlow
的应用场景。

示例中的应用场景是简化之后的,目的是演示
MoveAlongPath

PhaseFlow
的使用方式。

2.1. 模拟行星绕太阳公转

该示例使用
MoveAlongPath
创建了一个行星绕太阳公转的动画,通过定义椭圆轨道路径,让行星沿着该路径运动,模拟了天体运动场景。

# 创建太阳
sun = Dot(color=YELLOW, radius=0.5)
# 创建椭圆轨道
orbit_path = Ellipse(width=4, height=2)
# 创建行星
planet = Dot(color=BLUE, radius=0.2)
self.add(sun, orbit_path, planet)
# 让行星沿着椭圆轨道运动
self.play(
    MoveAlongPath(planet, orbit_path),
    run_time=5,
    rate_func=linear,
)

2.2. 展示函数曲线绘制过程

这个示例利用
MoveAlongPath
展示了函数曲线的绘制过程,通过沿着定义好的函数曲线路径移动一个点,帮助理解函数的形状。

# 定义函数(这里以y = x^2为例)
def f(x):
    return x**2

graph = axes.plot(
    f,
    x_range=[-2.5, 2.6],
    color=GREEN,
    stroke_width=2,
)
self.play(Create(graph))

# 创建一个点沿着曲线运动
moving_dot = Dot(color=YELLOW)
self.play(
    MoveAlongPath(moving_dot, graph),
    run_time=5,
    rate_func=linear,
)

2.3. 展示简单谐振子的相空间轨迹

这个示例借助
PhaseFlow
展示了简单谐振子在相空间中的轨迹,根据谐振子的微分方程定义了向量场,动画展示了系统状态在相空间中的变化。

# 定义谐振子的微分方程
def harmonic_oscillator_deriv(state):
    # x, v = state
    dxdt = state[1]
    dvdt = -1 * state[0]
    return np.array([dxdt, dvdt, 0])

# 创建相空间中的点(初始位置)
initial_state = np.array([1.0, 0.0])
state_dot = Dot(point=axes.c2p(*initial_state), color=YELLOW)
self.add(state_dot)
# 使用PhaseFlow展示相空间轨迹
self.play(
    PhaseFlow(
        harmonic_oscillator_deriv,
        state_dot,
        virtual_time=5,
        rate_func=linear,
    ),
    run_time=5,
)

2.4. 模拟单摆运动在相空间轨迹

这个示例使用
PhaseFlow
根据单摆的微分方程来展示其在相空间中的轨迹,以此模拟单摆运动在相空间的动态行为。

# 定义单摆的微分方程
def pendulum_deriv(state):
    theta, omega = state[:2]
    g = 9.8  # 重力加速度
    L = 1.0  # 单摆长度
    dtheta_dt = omega
    domega_dt = -g / L * np.sin(theta)
    return np.array([dtheta_dt, domega_dt, 0])

# 创建相空间中的点(初始位置)
# 初始角度为 45 度,初始角速度为 0
initial_state = np.array([np.pi / 4, 0.0]) 
state_dot = Dot(point=axes.c2p(*initial_state), color=YELLOW)
self.add(state_dot)

# 使用 PhaseFlow 展示单摆相空间轨迹
self.play(
    PhaseFlow(
        pendulum_deriv,
        state_dot,
        virtual_time=5,
        rate_func=linear,
    ),
    run_time=5,
)

3. 附件

文中的代码只是关键部分的截取,完整的代码共享在网盘中(
move.py
),

下载地址:
完整代码
(访问密码: 6872)

一:背景

1. 讲故事

在线程饥饿的场景中,我们首先要了解的就是线程是如何动态注入的?其实现如今的ThreadPool内部的实现逻辑非常复杂,而且随着版本的迭代内部逻辑也在不断的变化,有时候也没必要详细的去了解,只需在稍微宏观的角度去理解一下即可,我准备用三篇来详细的聊一聊
线程注入
的流程走向来作为
线程饥饿
的铺垫系列,这篇我们先从
Thread.Sleep
的角度观察线程的动态注入。

二:Sleep 角度下的动态注入

1. 测试代码

为了方便研究,我们用
Thread.Sleep
的方式阻塞线程池线程,然后观察线程的注入速度,参考代码如下:


        static void Main(string[] args)
        {
            for (int i = 0; i < 10000; i++)
            {
                ThreadPool.QueueUserWorkItem((idx) =>
                {
                    Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")} -> {idx}: 这是耗时任务");

                    Thread.Sleep(int.MaxValue);
                }, i);
            }

            Console.ReadLine();
        }

仔细观察卦中的输出,除了初始的12个线程喷涌而出,后面你会发现它的线程动态注入有时候大概是
500ms
一次,有时候会是
1000ms
一次,所以我们可以得到一个简单的结论:Thread.Sleep 场景下1s 大概会动态注入
1~2
个线程。

有了这个结论之后,接下来我们探究下它的底层逻辑在哪?

2. 底层代码逻辑在哪

千言万语不及一张图,截图如下:

接下来我们来聊一下卦中的各个元素吧。

  1. GateThread

在 PortableThreadPool 中有一个 GateThread 类,专门掌管着线程的动态注入,默认情况下它大概是 500ms 被唤醒一次。这个是有很多逻辑源码支撑的。


    private static class GateThread
    {
        public const uint GateActivitiesPeriodMs = 500;

        private static void GateThreadStart()
        {
            while (true)
            {
                bool wasSignaledToWake = DelayEvent.WaitOne((int)delayHelper.GetNextDelay(currentTimeMs));
                ...
            }
        }

        public uint GetNextDelay(int currentTimeMs)
        {
            uint elapsedMsSincePreviousGateActivities = (uint)(currentTimeMs - _previousGateActivitiesTimeMs);
            uint nextDelayForGateActivities =
                elapsedMsSincePreviousGateActivities < GateActivitiesPeriodMs
                    ? GateActivitiesPeriodMs - elapsedMsSincePreviousGateActivities
                    : 1;
            ...
        }
    }

  1. SufficientDelaySinceLastDequeue

这个方法是用来判断任务最后一次出队的时间,即内部的
lastDequeueTime
字段,这也是为什么有时候是1个周期(500ms),有时候是2个周期的底层原因,如果在一个周期内判断
lastDequeueTime(490ms)<=500ms
,那么在下一个周期内判断最后一次出队的时间自然就是
490ms+500ms
,所以这就是为什么 Console 上显示大约 1s 的间隔的原因了,下面的代码演示了 lastDequeueTime 是如何存取的。


        private static void GateThreadStart()
        {
            if (!disableStarvationDetection &&
                threadPoolInstance._pendingBlockingAdjustment == PendingBlockingAdjustment.None &&
                threadPoolInstance._separated.numRequestedWorkers > 0 &&
                SufficientDelaySinceLastDequeue(threadPoolInstance))
            {
                bool addWorker = false;
              
                if (addWorker)
                {
                    WorkerThread.MaybeAddWorkingWorker(threadPoolInstance);
                }
            }
        }
        private static bool SufficientDelaySinceLastDequeue(PortableThreadPool threadPoolInstance)
        {
            uint delay = (uint)(Environment.TickCount - threadPoolInstance._separated.lastDequeueTime);
            uint minimumDelay;
            if (threadPoolInstance._cpuUtilization < CpuUtilizationLow)
            {
                minimumDelay = GateActivitiesPeriodMs;
            }
            else
            {
                minimumDelay = (uint)threadPoolInstance._separated.counts.NumThreadsGoal * DequeueDelayThresholdMs;
            }

            return delay > minimumDelay;
        }
        private static void WorkerDoWork(PortableThreadPool threadPoolInstance, ref bool spinWait)
        {
            bool alreadyRemovedWorkingWorker = false;
            while (TakeActiveRequest(threadPoolInstance))
            {
                threadPoolInstance._separated.lastDequeueTime = Environment.TickCount;
                if (!ThreadPoolWorkQueue.Dispatch())
                {
                }
            }
        }

  1. CreateWorkerThread

这个方法是用来创建线程的主体逻辑,在线程池中由上层的 MaybeAddWorkingWorker 调用,参考如下:


        internal static void MaybeAddWorkingWorker(PortableThreadPool threadPoolInstance)
        {
            while (toCreate > 0)
            {
                CreateWorkerThread();
                toCreate--;
            }
        }

        private static void CreateWorkerThread()
        {
            Thread workerThread = new Thread(s_workerThreadStart);
            workerThread.IsThreadPoolThread = true;
            workerThread.IsBackground = true;
            workerThread.UnsafeStart();
        }

这里有一个注意点:上面的
while (toCreate > 0)
代码预示着一个周期内(500ms)可能会连续创建多个工作线程,但在饥饿的大多数情况下都是
toCreate=1
的情况。

3.如何眼见为实

说了这么多,能不能用一些手段让我眼见为实呢?要想眼见为实也不难,可以用 dnspy 断点日志功能观察即可,分别在如下三个方法上下断点。

  1. delayHelper.GetNextDelay

在此处下断点的目的用于观察 GateThread 的唤醒周期时间,截图如下:

  1. SufficientDelaySinceLastDequeue

这里下断点主要是观察当前的延迟如果超过 500ms 时是否真的会通过 CreateWorkerThread 创建线程。截图如下:

  1. WorkerThread.CreateWorkerThread

最后我们在 MaybeAddWorkingWorker 方法的底层的线程创建方法 CreateWorkerThread 中下一个断点。

所有的埋点下好之后,我们让程序跑起来,观察 output 窗口的输出。

从输出窗口中可以清晰的看到以500ms为界限判断啥时该创建,啥时不该创建。

三:总结

可能有些朋友很感慨,线程的动态注入咋怎么慢?1s才1-2个,难怪会出现线程饥饿。。。哈哈,下一篇我们聊一聊Task.Result下的注入优化。
图片名称

一、引言

处理大量数据是一个常见的需求,传统的同步处理方式往往效率低下,尤其是在数据量非常大的情况下。本篇将介绍一种高效的多线程异步处理大数据量的方法,通过边处理边消费的方式,极大地提高了处理效率,并且减少了内存开销。这种解决方案只是实现这一需求的一种实践,并不排除还有其他方式可以实现。如果您有任何问题或建议,欢迎在评论区留言讨论。

二、假设场景

假设我们有一个需要处理大量图片文件的应用程序。每个图片文件都需要进行压缩、调整等复杂的计算和数据处理。由于图片文件数量庞大,如果按同步方式处理,不仅速度慢,而且会占用大量内存。为了解决这个问题,我们采用了多线程异步处理的方式。

三、解决方案

我们可以使用 .NET 的
异步编程模型

Channel
来实现生产者-消费者模式。生产者负责读取图片文件并将其写入到
Channel
中,消费者从
Channel
中读取图片文件并进行处理。通过这种方式,我们可以边读取边处理,极大地提高了处理效率。

以下是解决问题的思路和方案:

  1. 定义生产者和消费者:
    • 生产者负责读取图片文件,并将其写入到
      Channel
    • 消费者从
      Channel
      中读取图片文件,并对其进行处理(如压缩、调整大小等)
  2. 使用
    Channel
    实现生产者-消费者模式:
    • Channel
      是 .NET 提供的一种用于实现生产者-消费者模式的高效数据结构
    • 生产者将数据写入
      Channel
      ,消费者从
      Channel
      中读取数据
  3. 并行处理:
    • 使用
      Task.Run
      启动多个生产者和消费者任务,以实现并行处理
    • 通过设置最大并行度来控制同时运行的任务数量
  4. 异步编程:
    • 使用
      async

      await
      关键字实现异步编程,以避免阻塞线程。
    • 异步编程可以提高应用程序的响应速度和吞吐量

涉及技术点介绍:

  • Channel
    :用于在生产者和消费者之间传递数据,支持高效的并发操作
  • Task
    :用于启动并行任务,实现多线程处理
  • async/await
    :用于实现异步编程,避免阻塞线程,提高应用程序的响应速度

四、示例代码

以下是一个简单的示例代码,演示如何使用
Channel
实现生产者-消费者模式来处理图片文件:

using System.Threading.Channels;

var cts = new CancellationTokenSource();
// 假设有一组图片文件
var imageFiles = Enumerable.Range(0, 1000).Select(x => $"image_{x}.jpg").ToList();

var processor = new ImageProcessor(10, cts.Token);
await processor.ProcessAsync(imageFiles);

Console.ReadKey();

/// <summary>
/// 图片处理器
/// </summary>
/// <param name="maxDegreeOfParallelism">最大并行度</param>
/// <param name="cancellationToken">CancellationToken</param>
public class ImageProcessor(int maxDegreeOfParallelism, CancellationToken cancellationToken)
{
    public async Task ProcessAsync(List<string> imageFiles)
    {
        // 创建一个无界的 Channel
        var channel = Channel.CreateUnbounded<string>();

        // 启动多个生产者任务
        var producerTasks = Enumerable.Range(0, maxDegreeOfParallelism)
            .Select(i => Task.Run(() => Producer(imageFiles, i, channel.Writer), cancellationToken))
            .ToArray();

        // 启动多个消费者任务
        var consumerTasks = Enumerable.Range(0, maxDegreeOfParallelism)
            .Select(_ => Task.Run(() => Consumer(channel.Reader), cancellationToken))
            .ToArray();

        // 等待所有生产者任务完成
        await Task.WhenAll(producerTasks);
        // 完成 Channel 的写入
        channel.Writer.Complete();
        // 等待所有消费者任务完成
        await Task.WhenAll(consumerTasks);
    }

    private async Task Producer(List<string> imageFiles, int producerIndex, ChannelWriter<string> writer)
    {
        try
        {
            // 计算每个生产者需要处理的文件数量
            int filesPerProducer = imageFiles.Count / maxDegreeOfParallelism;
            int start = producerIndex * filesPerProducer;
            int end = producerIndex == maxDegreeOfParallelism - 1
                ? imageFiles.Count
                : start + filesPerProducer;

            for (int i = start; i < end; i++)
            {
                // 模拟读取图片文件
                await Task.Delay(100, cancellationToken);
                // 将图片文件路径写入 Channel
                await writer.WriteAsync(imageFiles[i], cancellationToken);
                Console.WriteLine($"Producer image file: {imageFiles[i]}");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Producer error: {ex.Message}");
        }
    }

    private async Task Consumer(ChannelReader<string> reader)
    {
        try
        {
            // 从 Channel 中读取数据并处理
            await foreach (var imageFile in reader.ReadAllAsync(cancellationToken))
            {
                // 模拟处理图片文件(如压缩、调整大小等)
                await Task.Delay(100, cancellationToken);
                Console.WriteLine($"Processed image file: {imageFile}");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Consumer error: {ex.Message}");
        }
    }
}

〇、前言

本文继续整理下 K8s 的一些基础概念,作为前一篇概念汇总的补充。

前一篇博文链接
https://www.cnblogs.com/hnzhengfy/p/k8s_concept.html

一、详情

1.1 Label

Label 在 k8s 中是一个非常核心的概念,我们可以将 Label 指定到对应的资源对象中,例如 Node、Pod、Replica Set、Service 等,在配置文件中一般为 labels。

一个资源可以绑定任意个 Label,k8s 通过 Label 可实现多维度的资源分组管理,后续可通过 Label Selector 查询和筛选拥有某些 Label 的资源对象。

例如创建一个 Pod,给定一个 Label,workerid=123,后续可通过 workerid=123 删除拥有该标签的 Pod 资源。

参考:
https://www.cnblogs.com/objcoding/p/13567281.html

1.2 DaemonSet 守护进程

DaemonSet 是 k8s 中的一种控制器,用于管理 Pod 的部署,
确保每个节点上都有一个 Pod 在运行

DaemonSet 控制器会监视集群中的节点状态,
一旦有新的节点加入集群,或者节点状态发生变化(如节点重新启动),控制器就会触发相应操作

  • 创建 Pod:
    当检测到新节点时,控制器会在该节点上创建一个新的 Pod
    ,并确保每个节点上只运行一个 Pod 实例。
  • 更新 Pod:如果 DaemonSet 的配置发生变化,控制器会
    自动更新每个节点上的 Pod 实例
  • 删除 Pod:如果节点发生故障或者被删除,控制器会
    自动删除该节点上的 Pod 实例
  • 扩容和缩容:DaemonSet 还支持扩容和缩容,可以
    根据需要增加或减少 Pod 的数量

DaemonSet
常用于运行一些系统级别的服务或者监控应用程序,使集群中的服务更加健壮和可靠。
例如:

日志收集器:在每个节点上运行日志收集器(如 Fluentd 或 Filebeat),收集所有节点的日志数据,并将其发送到中心日志服务器进行存储和分析。
监控代理:在每个节点上运行监控代理(如 Prometheus Node Exporter 或 cAdvisor),收集所有节点的运行状态数据,并将其发送到中心监控服务器进行分析和展示。
网络代理:在每个节点上运行网络代理(如 kube-proxy 或 Istio Sidecar),负责节点之间的网络通信和流量管理。
安全代理:在每个节点上运行安全代理(如 Sysdig Falco 或 Aqua Security),检测所有节点的安全事件,并及时报警或进行防御

1.3 探针(Probe)

一个 Pod 被调度之后,就要进行初始化。初始化肯定是得有一个反馈的,否则都不知道最终有没有启动成功。
这些健康检查的功能,叫做探针(Probe)

常见的有 livenessProbe、readinessProbe、startupProbe 等三种探针。

  • livenessProbe 存活探针

LivenessProbe 用于检测容器是否仍然处于运行状态。如果探测失败,k8s 将根据 Pod 的重启策略决定是否重新启动该容器。

适用于需要监控容器内主进程或服务是否正常运行的情况。

例如,当宿主机故障或资源不足导致容器停止工作时,可以通过 LivenessProbe 来检测并采取相应的恢复措施。

livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 60
  periodSeconds: 10
  successThreshold: 1
  timeoutSeconds: 5

在这个示例中,kubelet 会每 10 秒发送一次 HTTP GET 请求到
/health
路径,如果连续失败 5 次,则认为容器不健康,并根据重启策略进行处理。

  • ReadinessProbe(就绪探针)

ReadinessProbe 用于
检测容器是否已经准备好接收流量
。如果探测失败,k8s 会将该 Pod 从 Service 的 Endpoints 列表中移除,直到它再次通过探测为止。

适用于需要在容器启动或重启后,等待其完全准备就绪,再开始接收流量的场景。

例如,在滚动更新过程中,新版本的容器需要通过 ReadinessProbe 验证后才能开始处理请求。

readinessProbe:
  exec:
    command: ["cat", "/tmp/ready"]
  initialDelaySeconds: 5
  periodSeconds: 10
  successThreshold: 1
  failureThreshold: 3

在这个示例中,kubelet 会每 10 秒执行一次
cat /tmp/ready
命令,如果连续失败 3 次,则认为容器未就绪。

  • StartupProbe(启动探针)

StartupProbe 用于判断容器内的应用程序,
是否已经成功启动并完成初始化任务
。它在容器启动初期生效,先于 LivenessProbe 和 ReadinessProbe。

适用于启动时间较长或启动过程中有复杂初始化序列的应用程序。StartupProbe 可以防止在这些应用程序还未完全启动时就被误判为不健康或就绪。

startupProbe:
  httpGet:
    path: /startup
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 15
  successThreshold: 1
  failureThreshold: 3

在这个示例中,kubelet 会在容器启动后等待 10 秒,然后每 15 秒发送一次 HTTP GET 请求到 /startup 路径,如果连续失败 3 次,则认为启动失败。

一般,花费 120s startupProbe 的启动实践,每隔 5s 检测一下 livenessProbe,每隔 10s 检测一下 readinessProbe,是常用的操作,一般都是在 yml 配置文件中进行详细配置。

参考: https://zhuanlan.zhihu.com/p/379270517

1.4 钩子(Hook)

钩子(Hook),主要有 PostStart 和 PreStop 两种。

PostStart 在容器启动后立即触发执行,用于完成启动后的初始化操作,例如加载配置、启动辅助进程。它与主进程无直接依赖关系,不会阻塞主进程启动。

lifecycle:
  postStart:
    exec:
      command: ["sh", "-c", "echo 'Container started'"]

PreStop 在容器收到终止信号(如 kubectl delete pod 或 kubectl scale)时触发执行,用于执行停止前的清理工作,例如保存状态、关闭连接、释放资源。

preStop:
  exec:
    command: ["sh", "-c", "echo 'Container stopping'; sleep 5"]

钩子是Kubernetes中一种强大的机制,能够在容器生命周期的特定阶段执行自定义操作。通过合理利用钩子,可以在容器启动后或停止前完成必要的初始化和清理工作,从而提高应用的可靠性和自动化水平。

其实 Hook 就是一些 shell 脚本,需要在指定的事件点执行,因为比较常用就升级到关键字级别了。

1.5 Stateful Sets

StatefulSet 是 k8s 中用于
管理有状态应用
的控制器,它提供了
稳定的网络标识符、持久化存储以及有序部署和扩展等功能

它与 deployment 类似,唯一的区别是 deployment 创建一组任意名称的 pod,并且 pod 的顺序对它来说并不重要。

而 StatefulSet 为每个 Pod 维护了一个有粘性的 ID,这些 Pod 是基于相同的规约创建的,但不可相互替换,且每个 Pod 都有一个永久不变的 ID。

如果要为 example 的 pod 创建 3 个副本,那么 StatefulSet 将会创建为:example-0、example-1、example-2。因此,这一创建方式最重要的好处就是你可以通过 pod 的名称就了解大致的情况。

两个核心组件:

  • Headless Service:
    无头服务(Headless Service)是一种特殊的 Service,其 ClusterIP 设置为 None,
    不会分配 Cluster IP,也不会进行负载均衡和路由
    。这种服务主要用于为 Pod 提供稳定的 DNS 记录,使得 Pod 可以通过域名进行访问。
  • VolumeClaimTemplates:StatefulSet 要求
    每个 Pod 都挂载持久化存储卷
    ,以确保数据在 Pod 重建时得以保留。VolumeClaimTemplates 允许为每个 Pod 动态创建 PersistentVolumeClaim(PVC),从而绑定到相应的 PersistentVolume(PV)。

主要特性:

  • 稳定的唯一网络标识符:每个 StatefulSet 的 Pod 都有一个稳定的网络标识符(如 DNS 名称),这个标识符由控制器自动生成,并与 Pod 的生命周期保持关联。这使得有状态应用更容易被其他应用或服务访问和发现。
  • 有序部署和扩展:StatefulSet 会按照指定的顺序
    逐个创建和更新
    Pod。每个 Pod 都有一个唯一的序号,用于标识其在集群中的位置。在扩展时,新的 Pod 会按照相同的顺序创建,确保有状态应用的数据一致性和可用性。
  • 稳定的存储:每个 StatefulSet 的 Pod 都可以使用持久卷(PersistentVolume)存储数据,这些存储可以在 Pod
    重新启动或迁移时保持不变
    。这使得有状态应用可以继续使用之前的数据,保证数据的持久性和可靠性。
  • 域名解析:每个 StatefulSet 的 Pod 都有一个
    稳定的域名
    ,可以通过该域名进行访问。域名的格式为<statefulset名称>-<序号>..svc.cluster.local>,这使得有状态应用可以通过域名进行服务发现和通信。
  • 有序删除:在删除 StatefulSet 时,控制器会按照指定的顺序逐个删除 Pod。这可以确保有状态应用在删除过程中不会丢失数据,并且能够有序地关闭服务。

StatefulSet 通过提供稳定的网络标识符、持久化存储以及有序部署和扩展等功能,为 k8s 中的有状态应用提供了强大的支持。

然而,在使用 StatefulSet 时也需要注意一些事项,如删除 StatefulSet 并不会自动删除其关联的 PVCs 和 PVs 等存储资源。因此,在删除 StatefulSet 前需要明确是否也需要删除这些存储资源并确保应用数据已经妥善备份或具备迁移数据的能力。

参考: https://www.infoq.cn/article/LwQsple5bRO5Vu9966w6

1.6 ConfigMap

ConfigMap 是 k8s 中一种用于配置管理的 API 资源对象,它允许用户
将配置信息与容器镜像解耦
,从而使得应用程序的配置更加灵活和可移植。用于存储非密钥/值数据,如配置文件、环境变量和命令行参数等。

通过合理地使用ConfigMap,可以提高应用程序的可移植性和可维护性,同时确保配置的安全性和一致性。

ConfigMap 的主要特点是,可以将应用程序的
配置信息以键值对的形式保存
,并且这些配置信息可以
独立于应用程序代码进行管理和更新

四种创建方式:

  • 命令行创建:可以通过 kubectl 命令行工具使用 --from-literal 参数来创建一个 ConfigMap,其中包含键值对。
  • 文件创建:可以从文件中创建 ConfigMap,支持单个文件或目录。
  • 目录创建:从目录创建 ConfigMap 时,只会读取文件夹第一级内容。
  • YAML 文件创建:可以使用 YAML 文件定义 ConfigMap 的内容和结构。

主要的作用:

  • 解耦配置与镜像:
    通过使用 ConfigMap,应用程序的配置信息不再硬编码在容器镜像中,而是存储在 k8s 集群中,这样可以在不修改容器镜像的情况下更新配置,更新完成后重启服务即可。
  • 提高安全性:敏感信息不应存储在 ConfigMap 中,因为它是明文存储没有特殊的安全措施,而应使用 Secrets 来保护,下文将详细介绍。
  • 适应不同环境:ConfigMap 可以用于不同环境应用环境的配置统一,通过使用不同的 ConfigMap,应用程序可以在不同的环境中保持一致的行为。

使用方式:

  • 作为环境变量注入:Pod 可以通过 envFrom 字段引用 ConfigMap,将其所有数据定义为容器的环境变量。
  • 作为命令行参数传递:Pod 可以将 ConfigMap 的数据保存在环境变量中,然后通过 $(VAR_NAME) 的方式引用环境变量。
  • 作为 Volume 挂载:Pod 可以通过 volumeMounts 将 ConfigMap 作为文件或目录挂载到容器内部。

1.7 Secrets

Secrets 是 k8s 中的一种资源类型,
专门用于存储和管理敏感信息
。这些信息通常包括密码、OAuth 令牌以及 SSH 密钥等,它们被 Base64 编码后存储在 k8s 集群中。

通过使用 Secrets,可以避免将这些敏感信息硬编码在应用程序代码或 Docker 镜像中,从而提高了安全性和灵活性,同时也可确保配置的灵活性和可移植性。

三种创建方式:

  • 命令行创建:可以通过 kubectl 命令行工具使用 --from-literal 参数来创建一个 Secret,其中包含键值对。
  • 文件创建:可以从文件中创建 Secret,支持单个文件或目录。
  • YAML 文件创建:可以使用 YAML 文件定义 Secret 的内容和结构。

主要的四种类型:

  • Opaque 类型:用户定义的任意数据,通常用于存储密码、秘钥等敏感信息。
  • Service Account:由 k8s 自动创建,用于访问 API 服务器,并会自动挂载到 Pod 的 /run/secrets/kubernetes.io/serviceaccount 目录中。
  • kubernetes.io/dockerconfigjson:用来存储私有 Docker Registry 的认证信息。
  • kubernetes.io/tls:用于存储 TLS 证书和其关联的私钥。

使用方式:

  • 作为环境变量注入:Pod 可以通过 envFrom 字段引用 Secret,将其所有数据定义为容器的环境变量。
  • 作为 Volume 挂载:Pod 可以将 Secret 的数据保存在环境变量中,然后通过 $(VAR_NAME) 的方式引用环境变量。
  • 作为镜像拉取凭证:允许 kubelet 从私有镜像仓库中拉取镜像。

Secrets 的安全性控制,数据以 Base64 编码格式存储,减少了直接暴露敏感信息的风险。同时也结合 k8s 的 RBAC(基于角色的访问控制)策略,可以限制 Secrets 的访问权限,确保只有授权的用户或服务才能访问。