UEFI 启动的各阶段介绍

thbcm阅读(380)

UEFI 启动的各阶段介绍

UEFI启动的七个阶段介绍

系统固件开发学习系列:

一、EDKII环境搭建 & QEMU虚拟机双平台安装

二、EDKII工程结构介绍

目录

一、整体概念

二、SEC 安全初始化阶段

三、PEI 预 EFI 初始化阶段

四、DXE—驱动执行环境阶段

五、BDS—启动设备选择阶段

六、TSL—瞬态系统加载阶段

七、RT 和 AL 阶段

一、整体概念

UEFI启动的七个阶段分别为:

  1. SEC (Security),安全初始化阶段。
  2. PEI (Pre-EFI Initialization),预EFI初始化阶段。
  3. DXE (Driver Execution Envirenment),驱动执行环境阶段。
  4. BDS (Boot Device Selection),启动设备选择阶段。
  5. TSL (Transient System Load),操作系统加载前期阶段。
  6. RT (Runtime) ,运行时阶段。
  7. AL(After Life):关闭阶段(系统关机或重启)

首先对以上启动步骤进行一个大概的理解,以便对整个系统有全局性的思考。

SEC 阶段,负责处理系统复位、CPU 初始化和临时内存的搭建,为后续阶段奠定安全可信的执行基础;

紧接着是 PEI 阶段,它在资源极度受限的环境中初始化内存控制器等关键硬件,并将系统信息通过 HOB 列表传递给下一棒;

随后进入核心的 DXE 阶段,大量驱动程序在此被执行,全面初始化平台硬件并构建出丰富的Boot Services与Runtime Services服务框架;

硬件就绪后,BDS阶段接管,它初始化控制台设备、管理启动项,并依据策略加载操作系统的引导程序(.efi文件);

引导程序开始执行便进入了TSL阶段,它作为临时系统加载环境,依然利用Boot Services进行内核加载等准备工作;

准备就绪后,引导程序调用ExitBootServices(),系统进入RT阶段,此时Runtime Services依然可用,但Boot Services已被卸载,操作系统内核开始掌控全局;

最终,当操作系统完全启动后,若系统进入睡眠或关机状态,则会在需要时进入AL阶段,由固件提供特定的运行时错误处理或睡眠恢复支持。

二、SEC—安全初始化阶段

2.1 简介

系统上电后,CPU 处于最原始的硬件状态(如 x86 的实模式),此时 DRAM(主内存)及外设均未初始化。SEC(Security)阶段作为UEFI固件执行的第一个环节,其核心任务就是将CPU从这种不可控的上电状态,转变为一个可执行代码的稳定环境,并为后续的 PEI(Pre-EFI Initialization)阶段准备好执行环境,其实也类似于 ARM 嵌入式设备的启动,start.S 汇编入口文件。

SEC阶段的代码位于主板 SPI Flash 的 Boot Block 区域,体积小(通常几十 KB)且拥有最高执行权限。其执行流程如下:

绝大多数主板使用 SPI Flash 存储固件。容量多为8~64MB,速度几十 MB/s,接口线4根,可擦写。

  1. 建立临时执行环境:由于 DRAM 尚未可用,SEC 阶段利用 Cache-as-RAM(CAR)技术,将CPU的内部高速缓存配置为临时的读写内存(即栈空间),从而为 C 语言等高级语言的运行建立最基本的执行环境。
  2. 执行核心初始化:使用汇编语言完成 CPU 关键寄存器的初始化,确保处理器处于稳定、可预测的状态。
  3. 安全验证与交接:在将控制权移交前,SEC 阶段会校验PEI核心镜像的哈希值,以确保固件代码的完整性和安全性。
  4. 传递控制权:最后,SEC 系统当前状态、BFV(Boot Firmware Volume)地址以及临时 RAM 区域等关键参数传递给 PEI 阶段,并跳转至 PEI 入口点,完成其历史使命。

实模式:x86 CPU 启动后的初始状态。实模式下,CPU能直接访问的内存空间只有 1MB,使用“段:偏移”的方式寻址,没有内存保护,并且只能进行单任务处理。实模式目前的主要作用是系统启动。当你打开电脑时,现代 CPU(即使是 64 位的)会自动进入实模式。这时,BIOS 或 UEFI 固件会进行硬件自检(POST),然后加载操作系统的引导程序。这个引导程序最初也是在实模式下运行的。一旦操作系统接管,它就会功成身退,让位于更高级的保护模式或长模式。

2.2 具体步骤

  • 首先进行 CPU 初始化,禁止 CPU 的段寄存器,禁止中断,清除状态寄存器,开启 Cache,关闭不安全的功能(如调试端口)。然后建立临时栈(Cache-As-RAM, CAR),因为此时 DRAM 还未初始化,而代码执行又需要栈空间,SEC 阶段需要用 CPU Cache 模拟RAM。

    CAR 技术的实现方式是,通过 MSR(Model Specific Register)配置Cache进入“Write-Back”模式。将 Cache 中的一部分地址空间映射成伪地址,在该区域内建立堆栈,此时 CPU 就可以正常执行函数调用,存放临时变量。

  • 在建立执行环境之后,需要验证固件的完整性。安全是 SEC 阶段名称的由来,在现代系统中,主板固件是攻击者的目标之一。固件制造商会在 Flash 中存放公钥(PK),UEFI 镜像(PEI Core 及 DXE Core)使用私钥签名,SEC 阶段计算哈希(SHA256)并验证签名,若验证失败,立即终止启动。

    SHA256:一种著名的加密哈希函数。

  • 部分主板允许在 SEC 阶段输出调试信息,例如使用串口输出 “SEC Start”,提供“Early Debug”功能。在商用系统中通常被关闭。

  • SEC 会判断当前的启动属于哪种情况:正常启动,冷启动(断电重启),热启动(软重启(CTRL+ALT+DEL)),S3 恢复(从休眠唤醒),恢复模式(由于错误进入 Recovery 路径)。这些信息会被打包成结构体传给 PEI Core。

  • 最后,SEC 阶段通过一个标准的入口将控制权交给 PEI 核心。

// SecCoreData: SEC 阶段生成的系统信息(栈、内存范围、HOB 起始地址)
// PpiList: 提供给 PEI 的功能接口(如临时存储、调试等)
PeiCoreEntryPoint (SecCoreData, PpiList)

SEC 阶段重要的数据结构:

/**
 * brief: 描述 Cache-As-RAM 区域,栈区,提供固件卷(Firmware Volume, FV)的地址。
 */
typedef struct {
  EFI_PHYSICAL_ADDRESS   DataSize;
  EFI_PHYSICAL_ADDRESS   PeiTemporaryRamBase;
  EFI_PHYSICAL_ADDRESS   PeiTemporaryRamSize;
  EFI_PHYSICAL_ADDRESS   StackBase;
  EFI_PHYSICAL_ADDRESS   StackSize;
  EFI_PHYSICAL_ADDRESS   BootFirmwareVolumeBase;
  EFI_PHYSICAL_ADDRESS   BootFirmwareVolumeSize;
} EFI_SEC_PEI_HAND_OFF;

在 OVMF 中,SEC 阶段的入口文件为OvmfPkg/Sec/Secmain.c。其主要逻辑如下:

VOID
SecMain (
  IN UINT32 SizeOfRam,
  IN UINT32 StackBase
  )
{
  InitializeCpu ();             // CPU 初始化
  InitializeCacheAsRam ();      // 建立Cache-As-RAM
  VerifyFirmwareIntegrity ();   // 固件验证
  DetectBootMode ();            // 检测启动类型
  HandOffToPeiCore ();          // 跳转至PEI阶段
}

三、PEI—预 EFI 初始化阶段

3.1 简介

PEI 阶段是 UEFI 启动流程中最关键的过渡阶段之一,它的任务是让系统从没有内存,几乎什么都不能做的状态,过渡到可以执行复杂 C 语言代码,和加载驱动的 DXE 阶段。当系统从 SEC 阶段过来后,PEI 模块的入口函数首先会执行,初始化一个称为 PEI 核心的数据结构,作为 PEI 阶段的中枢管理器,负责调度后续所有 PEI 模块的执行。由于此时 DRAM 还不能使用,CPU 只能访问 CPU Cache,所有 PEI 模块都被压缩存放在固件设备中,由 PEI 核心按需解压到 Cache 中去运行。PEI 阶段的首要任务就是就行内存控制器的初始化。具体来说,PEI 模块通过硬编码或从非易失性存储器中读取的内存时序参数,逐步配置内存控制器,最终完成物理内存的检测与初始化。在内存就绪前后,PEI 阶段会执行一系列关键的硬件初始化操作,包括配置芯片组的基本寄存器,检测并初始化 BFV 中的其他 PEIM,处理固件中的安全策略,以及构建一份名为 HOB 的 Hand-Off Block 数据结构链表。HOB 列表至关重要,其以只读的方式将 PEI 阶段得到的系统硬件信息—内存布局,CPU 特性,设备状态等,完整地传递给 DXE 阶段。一旦内存初始化完成,HOB 列表构建完毕,PEI 阶段的最终任务就是定位并调用 DXE 初始程序加载器的入口函数,将系统控制权彻底移交至 DXE 阶段。

3.2 关键点 & 执行流程

  • PEI 阶段入口。_ModuleEntryPoint()_pei_main(),在 SEC 阶段完成之后,会将一些信息传入 PEI 阶段,如临时内存的基地址和大小,Boot Mode,固件卷基地址,栈顶指针,是否为冷启动等。

  • PEI 阶段由两类模块组成,分别为PEI Core(PEI Foundation),和PEIMs(PEI Modules)。前者为 PEI 的内核,负责调度 PEIM,创建HOB,加载DXE。后者为各种硬件初始化模块,如内存控制器,芯片组,I/O 控制器等。PEI Core 会调用一系列PEIM。这些模块由EFI_PEI_PPI_DESCRIPTOR描述。

  • 一些关键数据结构。

    /**
     * HOB 用于项 DXE 传递硬件资源信息
     * 下面是一些常见的 HOB 类型,这些 HOB 最后会被 DXE Core 读取,用来构建 UEFI 系统表。
     */
    EFI_HOM_MEMORY_ALLOCATION		// 内存分配信息
    EFI_HOB_RESOURCE_DESCRIPTOR		// 物理内存资源描述
    EFI_HOB_GUID_TYPE				// 自定义结构(比如SMBIOS、ACPI表等)
    EFI_HOB_FIRMWARE_VOLUME			// 固件卷位置
    EFI_HOB_CPU						// CPU 信息(寄存器宽度、特性)
        
    /**
     * PPI(PEI-to-PEI Interface)
     * PEI 阶段存在诸多模块,PPI 用于这些模块之间的通信
     * PEI Core 会根据 GUID 调用这些接口。
     */
    EFI_PEI_PPI_DESCRIPTOR gMemoryInitPpi = {
        EFI_PEI_PPI_DESCRIPTOR_PPI,
        &gEfiPeiMemoryInitPpiGuid,
        &PeiMemoryInitInterface
    };
    

GUID(Globally Unique Identifier,全局唯一标识符)是一个 128 位的唯一编号,UEFI 使用它来唯一标识:各种模块(PEIM、PPI、Protocol),各种数据结构(比如 HOB、FV 文件、变量等)

例如:#define EFI_PEI_READ_ONLY_VARIABLE2_PPI_GUID
{ 0x2ab86ef5, 0xecb5, 0x4134, {0xb5, 0x89, 0x39, 0x1e, 0x29, 0x8b, 0xfa, 0x88} }

PEIM之间靠 GUID 来标识和查找接口,而不是靠函数名或文件路径。

执行流程:

  1. SEC 阶段完成 CPU 栈初始化,将临时内存参数传给 PEI,跳转到PeiCore()

  2. 建立临时内存(栈,全局变量区),CAR。

  3. PEI Core 解析 Firmware Volume,查找 .PEIM 模块,调用其中的入口函数,执行例如

    • MemoryInitPeim → 初始化 DRAM;

    • CpuInitPeim → 配置 CPU;

    • PchInitPeim → 初始化芯片组;

    • BoardInitPeim → 板级设备配置。

  4. 内存初始化完成之后 PEI Core 会通过 BuildHob() 系列函数创建各种 HOB。记录系统物理内存布局、固件卷地址、CPU信息等。

    BuildResourceDescriptorHob(...);
    BuildMemoryAllocationHob(...);
    BuildCpuHob(...);
    
  5. PEI Core 搜索包含 DXE Core 的固件卷,创建一个EFI_HOB_FIRMWARE_VOLUME,将该区域标记为可执行代码段。

  6. 将堆栈和数据从 Cache 搬到 DRAM,更新指针,释放临时 RAM。

  7. 进入 DXE 阶段。

典型文件:

SEC Core			MdeModulePkg/Core/Sec/SecMain.c			最早入口
PEI Core			MdeModulePkg/Core/Pei/PeiMain.c			PEI主循环与调度
MemoryInitPeim		MdeModulePkg/Universal/MemoryInitPei/	内存初始化模块
HobLib				MdePkg/Library/HobLib/					创建 HOB 的库函数
PeiServicesLib		MdePkg/Library/PeiServicesLib/			提供 PEIM 注册、PPI 管理接口

四、DXE—驱动执行环境阶段

4.1 简介

DEX 阶段是 UEFI 固件初始化的核心与主体,其核心使命是通过加载大量的驱动程序,来全面初始化处理器,芯片组及各类平台硬件,并构建出丰富的 Boot Services 与 Runtime Services 服务框架,最终为操作系统的加载做好一切准备。

System Table 是总表;
Boot Services 是启动期间的 API 集合,可供启动期间的程序调用;
Runtime Services 是 OS 运行期间的 API 集合,在操作系统启动之后,会为其创建虚拟地址空间映射,仍然可以调用相关函数。

具体介绍见下文。

DEX 在接受 PEI 阶段提交的 HOB 列表后,其便在一个功能完备的内存环境中展开。DEX 首先会初始化一系列基础服务,如驱动程序调度器,句柄数据库,Protocol 接口管理体系等,为后续驱动程序的有序加载和交互提供基础设施。随后,系统会进入一个密集的驱动程序发现与执行期。固件会从多个 Firmware Volume 中扫描加载大量的 DXE 驱动程序,这些程序遵循依赖关系被有序调度,逐一执行,从而完整对所有 CPU 高级功能,芯片组复杂单元,总线控制器,磁盘设备,网络接口以及输入输出设备等平台硬件的深度检测,初始化和驱动。在此过程中,每个驱动程序通常会将其提供的功能以 Protocol 的形式安装到系统数据库中,其他驱动或应用程序可以通过定位这些 Protocol 来使用相应的服务,这种模块化的设计使得硬件初始化和服务构建高效灵活可扩展。在硬件资源就绪后,DXE 会逐步构建并完善 Boot ServicesRuntime Services,前者为操作系统加载器提供内存管理,协议接口,事件定时等关键服务,后者包含在操作系统运行后仍需固件管理的功能。在所有的驱动程序成功加载并执行完毕之后,DXE 阶段的核心任务编宣告完成,系统将控制权交给 BDS阶段。

4.2 关键点介绍

DEX 阶段是 UEFI 的“核心操作系统内核”,负责建立完整的驱动执行环境,并初始化所有系统资源,为加载 Bootloader 做准备。

DEX 阶段的执行环境:

  • 内存已完全初始化;
  • MMU、Cache 已启用;
  • 可以使用堆、栈;
  • 支持动态加载 .efi 驱动;
  • 支持中断(部分架构)。

DEX 阶段的核心:

  • 初始化 CPU,内存,总线控制器(从内向外,包括MMU、Cache、PCI、USB、SATA 等);
  • 构建 EFI System Table(建立 UEFI 的核心接口表,供后续驱动,应用调用);
  • 加载 DXE 驱动(从 FV 中解析并执行);
  • 管理协议(Protocol),不同驱动之间通过 GUID 协议通信(类似面向对象接口);
  • 启动 BDS 阶段。

三大核心模块:

  1. DEX Core:管理驱动加载,协议注册,服务调度。

    其就像一个迷你的操作系统内核,它建立全局数据结构(EFI_SYSTEM_TABLE, Boot Services, Runtime Services),管理内存分配与页表,维护“Protocol Database”——所有 GUID 协议的注册表,提供动态驱动加载,卸载和通信机制。它在 PEI 阶段结束时被加载,加载入口由 PEI 阶段的 HOB 提供。

  2. DEX Dispatcher:调度器,扫描 FV 中的 DXE 驱动,并按依赖关系执行。

    DXE Dispatcher 像一个动态驱动管理器,扫描固件卷中所有的 DXE Driver文件(.efi),根据文件中的 Dependency Section 判断依赖关系,按照依赖顺序逐个加载,执行驱动,类似于 Linux 中的 modprobe。

  3. DEX Drivers:各种功能驱动模块PCI、USB、图形、文件系统、网络、变量服务等。

    几乎所有硬件的驱动,服务都在这里被初始化,如下表所示。每个驱动都是一个独立的 efi 文件,包含驱动入口函数,一组协议实现(用 GUID 标识),安装、注册、回调机制。

    分类 示例
    CPU/Chipset MTRR、Cache、中断控制器
    内存 内存映射服务
    PCI/USB/SATA 总线控制器驱动
    图形 GOP (Graphics Output Protocol)
    文件系统 Simple File System Protocol
    网络 PXE Base Code Protocol
    变量存储 Variable Services Protocol
    安全 Secure Boot Protocol

Protocol

前面多次提到 Protocol,这是 DXE 阶段的通信机制。先前我们介绍过 PEI 阶段的通信机制 PPI,接下来我们对 Protocol 进行简要说明,以对 DXE 阶段的通信有一个初步的理解。

/* Protocol 的结构: */
typedef struct {
  EFI_STATUS (EFIAPI *Start)(...);
  EFI_STATUS (EFIAPI *Stop)(...);
  ...
} EFI_DRIVER_PROTOCOL;

/* 每个协议由一个 GUID 唯一标识 */
#define EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID \
  { 0x9042a9de, 0x23dc, 0x4a38, {0x96,0xfb,0x7a,0xde,0xd0,0x80,0x51,0x6a} }

/* 安装协议 */
gBS->InstallProtocolInterface(Handle, &gEfiGraphicsOutputProtocolGuid, EFI_NATIVE_INTERFACE, &Gop);

/* 查找协议 */
EFI_GRAPHICS_OUTPUT_PROTOCOL *Gop;
gBS->LocateProtocol(&gEfiGraphicsOutputProtocolGuid, NULL, (VOID **)&Gop);

这里对 PPI 以及 Protocol 的介绍较为简略,本文主要学习对 UEFI 的启动过程,细节内容后续学习过程中会陆续补充。

重要的全局结构:

EFI_SYSTEM_TABLEBoot ServicesRuntime Services

系统表:

当 UEFI 应用(如 Bootloader)运行时,会把这个表作为参数传入efi_main(),让应用可以访问 UEFI 服务。

/* 1. EFI系统表 EFI_SYSTEM_TABLE 
 * 操作系统启动后,主要通过系统表与固件交互
 */
typedef struct {
    EFI_TABLE_HEADER                Hdr;             // 表头信息(签名、版本、大小)
    CHAR16                          *FirmwareVendor; // 固件厂商
    UINT32                          FirmwareRevision;// 固件版本号

    EFI_HANDLE                      ConsoleInHandle; // 控制台输入设备句柄
    EFI_SIMPLE_TEXT_INPUT_PROTOCOL  *ConIn;          // 输入协议接口(键盘输入)

    EFI_HANDLE                      ConsoleOutHandle;// 控制台输出设备句柄
    EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut;         // 输出协议接口(屏幕输出)

    EFI_HANDLE                      StandardErrorHandle;
    EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *StdErr;         // 标准错误输出

    EFI_RUNTIME_SERVICES            *RuntimeServices;// 运行时服务表指针
    EFI_BOOT_SERVICES               *BootServices;   // 引导服务表指针

    UINTN                           NumberOfTableEntries;
    EFI_CONFIGURATION_TABLE         *ConfigurationTable;
} EFI_SYSTEM_TABLE;
Boot Services:

它是一个结构体EFI_BOOT_SERVICES,它在系统还没有“交出控制权”给操作系统之前,提供大量的底层服务接口,也就相当于一系列 API,提供上层调用,调用方式如下。

功能类别 示例函数 作用
内存管理 AllocatePages() / FreePages() 分配或释放物理页
事件与定时 CreateEvent() / WaitForEvent() 创建、等待事件
协议操作 InstallProtocolInterface() / LocateProtocol() 安装、查找驱动协议
句柄操作 HandleProtocol() / LocateHandle() 查找设备句柄
加载执行 LoadImage() / StartImage() 加载并执行 EFI 应用或驱动
设备访问 OpenProtocol() / CloseProtocol() 访问驱动协议接口
退出引导服务 ExitBootServices() 结束 Boot Services,进入操作系统
/* 2. Boot Services
- 提供内存分配,事件、协议管理、句柄操作等功能。
- 在 OS Loader 阶段使用;
- 在 ExitBootServices() 调用后失效。
*/
// Boot Services提供接口调用方式举例
EFI_STATUS Status;
EFI_PHYSICAL_ADDRESS Addr = 0x100000;
Status = SystemTable->BootServices->AllocatePages(AllocateAnyPages, EfiLoaderData, 1, &Addr);
Runtime Services:

Runtime Services 同样是一个结构体 EFI_RUNTIME_SERVICES。与 Boot Services 不同的是,它提供的服务在操作系统启动之后仍然存在,这些函数的实现被 OS 的 UEFI 驱动或内核映射进虚拟地址空间。部分服务如下。

功能类别 示例函数 说明
时间日期 GetTime() / SetTime() 获取或设置系统时间
变量服务 GetVariable() / SetVariable() 访问 NVRAM 变量
系统复位 ResetSystem() 重启、关机等操作
虚拟地址转换 SetVirtualAddressMap() OS 建立虚拟内存时使用
固件更新 UpdateCapsule() 支持 BIOS/UEFI 更新机制
/* 3. Runtime Services
- 提供变量访问、时间服务、系统重启等功能;
- 在 OS 运行时仍可使用;
- 驻留在固件中,操作系统通过 SMM 调用。

五、BDS—启动设备选择阶段

5.1 简介

BDS 阶段是连接平台硬件初始化完成与操作系统加载器执行的桥梁,核心使命是初始化控制台设备,管理启动选项,并依据预设的启动策略来定位并执行目标操作系统的引导程序。当 DXE 阶段将所有必要的硬件驱动和 Boot Services 都准备好之后,系统控制权移交至 BDS,BDS 首先会执行一系列全局性系统初始化,包括图形控制台需要的显卡,键盘,鼠标等。随后,BDS 会加载和执行特定的驱动和应用程序,进一步枚举和初始化更多的设备,如 USB,网卡等,这些设备在 DXE 阶段未被强制初始化,但对于发现启动项至关重要。在设备就绪后,BDS 核心逻辑会根据 NVRAM 中存储的 UEFI 启动变量来搜集和整理所有可用的启动选项,这些选项可能来自硬盘上的 EFI 系统分区,网络服务器,光驱或者其他可引导介质。随后,BDS 会依据启动策略(例如默认启动,用户手动选择或单一启动)来尝试执行这些启动项。它会按照优先级顺序,逐个加载每个启动项对应的 OS 引导程序至内存中(如Windows的bootmgfw.efi或Linux的grubx64.efi)。一旦某个引导程序被成功加载并执行,BDS阶段便将其辛苦构建的整个Boot Services环境移交给该引导程序。至此,BDS阶段的历史任务宣告完成,系统的控制权正式过渡给操作系统的引导装载程序。

5.2 关键点介绍

BDS 的核心指责是根据系统配置和启动策略,选择合适的启动设备,并执行对应的启动加载程序。
在 EDK2 中,BDS 阶段主要通过以下文件实现:MdeModulePkg/Core/DxeBds/

核心函数入口:

VOID EFIAPI BdsEntry (
  IN EFI_BDS_ARCH_PROTOCOL  *This
)
// 流程包括:
BdsInitialize()				// 初始化控制台设备;
BdsBootDeviceSelect()		// 启动设备选择;
BdsBootViaBootOption()		// 尝试加载 Boot####;
BdsBootSuccessHandler()		// BdsBootFailHandler():处理成功或失败结果;
BdsEnterFrontPage()			// 用户交互界面。

具体任务

  1. 创建控制台设备(Console)。确定系统的输入输出设备,键盘,显示器,串口等。建立标准的输入输出句柄(ConIn、ConOut、StdErr),使得用户可以看到 BIOS/UEFI 的启动画面、输入命令。

  2. 加载并执行 Boot Manager(引导管理器)。这是 UEFI 规范规定的,系统固件应该包括一个引导管理器,负责引导顺序管理。它读取 Boot OrderBoot Option 等 NVRAM 变量,判断从哪个设备启动。

  3. 枚举所有 Boot Option(启动选项)。这些选项保存在 UEFI 变量中,如:

    BootOrder       // 启动顺序,例如 [0001, 0003, 0000]
    Boot0000        // 启动项0,例如 EFI Shell
    Boot0001        // 启动项1,例如 Windows Boot Manager
    Boot0003        // 启动项3,例如 UEFI USB Device
    

    每个 Boot Option 包含启动设备路径(EFI_DEVICE_PATH)和加载映像文件路径。

  4. 尝试加载启动项中的 EFI 可执行文件。BDS 通过 Boot Services 的 LoadImage()StartImage() 来加载 EFI 应用(例如 bootx64.efi)。通常这些文件存放在 \EFI\BOOT\BOOTX64.EFI,\EFI\Microsoft\Boot\bootmgfw.efi。如果加载失败,就尝试下一个 Boot Option。

  5. 处理 Boot Maintenance Manager (BMM)。若所有 Boot Option 均失败,BDS 会进入 BMM(类似 BIOS Setup 界面)。允许用户修改启动顺序、设置 NVRAM 启动变量、更新 Boot Option。

  6. 最终移交控制权。成功加载 Bootloader(例如 GRUB、Windows Boot Manager)后:调用 ExitBootServices(),销毁 Boot Services,把控制权交给 Bootloader,进入操作系统加载阶段。

关键数据结构

  1. Boot Option(启动项结构)

    typedef struct {
        UINT32 Attributes;
        UINT16 FilePathListLength;
        CHAR16 Description[];          				// 启动项名称(如 "Windows Boot Manager")
        EFI_DEVICE_PATH_PROTOCOL FilePathList[]; 	// 启动文件路径
        UINT8 OptionalData[];         				// 启动时需要传入的参数
    } EFI_LOAD_OPTION;
    
  2. BootOrder:存储在 NVRAM 中的启动顺序表。

    Boot Order 启动项编号 含义
    0001 Windows Boot Manager 启动 Windows
    0003 UEFI USB Device 从 USB 启动
    0000 EFI Shell 启动到 UEFI Shell

六、TSL—瞬态系统加载阶段

6.1 简介

TSL 是 BDS 阶段成功找到并开始执行操作系统引导程序(如.efi文件)后,直到该引导程序将系统控制权正式移交给操作系统内核之前的一个临时性的过渡执行环境。在此阶段,操作系统的引导程序(例如 GRUB 或 Windows Boot Manager)作为 UEFI 环境下的一个特殊应用程序,依然完整地运行在 UEFI Boot Services 所构建的富运行时环境之中,它可以自由地调用 Boot Services 提供的内存管理、协议接口和磁盘I/O等服务,来完成诸如加载操作系统内核、初始化文件系统、解析配置文件等关键准备工作;然而,TSL阶段的“临时”性也正体现在此,其核心目标是为最终告别 UEFI 环境并启动真正的操作系统做准备,因此,一旦引导程序完成了所有必要的加载任务,它就必须主动调用 ExitBootServices() 函数,这个关键调用标志着 TSL 阶段的终结。系统控制权由此正式移交,从而进入不再依赖UEFI固件的RT阶段

6.2 关键点介绍

TSL 是瞬态的,它只在交接控制权的瞬间存在。

主要任务

  1. 执行 BootLoader。BDS 阶段通过 LoadImage() + StartImage() 启动 BootLoader,BootLoader 在 UEFI 环境中执行,它是一个标准的 EFI 应用程序。这意味着 BootLoader 也能使用 UEFI 提供的 Boot Services 和 Runtime Services。
  2. 准备加载操作系统内核,BootLoader(例如 GRUB 或 Windows Boot Manager)会解析配置文件,选择内核映像(kernel image),加载内核到内存,设置命令行参数、RAMDisk、ACPI 表、内存映射等信息,准备 CPU 状态(模式、分页、栈等)。
  3. 退出 UEFI 环境。调用 ExitBootServices()。此时,所有 Boot Services 被销毁,只剩下 Runtime Services
  4. 跳转到操作系统入口点。

一些关键函数

  1. GetMemoryMap()。在调用 ExitBootServices() 之前,BootLoader 必须 先调用这个函数,获取当前系统的内存布局,返回一个 MapKey,这个 MapKey 是 ExitBootServices 的凭证(必须匹配当前内存状态)。
  2. ExitBootServices(),退出 UEFI 环境。通知 UEFI 固件释放所有 Boot Services,停止所有驱动、定时器、中断等,将内存管理完全交给 BootLoader/OS,仅保留 Runtime Services(例如时间、变量等功能)。
  • ExitBootServices()调用前后内存映射的变化。
  • ExitBootServices() 之前:所有固件驱动、服务都在运行,Boot Services 负责内存分配、设备管理,内存中存在大量 EFI 类型的区域,如 EfiBootServicesData
  • ExitBootServices() 之后:固件停止介入,所有 Boot Services 相关区域被标记为 “可重用”,BootLoader 或 OS 内核接管内存管理,仅剩 EfiRuntimeServicesCode/Data 区域仍被保留,供 OS 使用。

七、RT 和 AL 阶段

RT 阶段是在操作系统已经接过控制权之后,不做过多介绍,简单说一下此时固件还能作用的 Runtime Services 的相关函数。

功能类别 典型函数 作用
时间服务 GetTime(), SetTime() 获取/设置系统时间(CMOS时钟)
虚拟内存服务 SetVirtualAddressMap() 固件映射从物理地址切换到虚拟地址(OS启用分页后使用)
变量服务 GetVariable(), SetVariable() 读取/写入NVRAM变量(如启动项、SecureBoot状态)
重置服务 ResetSystem() 控制系统重启、关机、S3睡眠等
字符串服务 (已废弃) 早期版本中用于输出信息

AL 阶段是操作系统关闭电源或重启前的最后一个阶段。其通常在以下情况发生:

  1. 用户执行关机(Shutdown);
  2. 系统执行重启(Reboot);
  3. 系统进入睡眠/休眠(S3/S4);
  4. 系统掉电(Power Loss)或崩溃。

AL 阶段控制权变化:

时间点 控制权归属 状态
OS 正常运行 操作系统 使用 Runtime Services
调用 ResetSystem() 操作系统 → 固件 固件开始复位
重启/关机完成 固件接管 重新进入 SEC 阶段或断电

结语

本文较为详细介绍了 UFEI 规范中启动的主要流程,由于本人是初学者,很多地方理解的不是很深刻,未来学习中会持续修改补充。本文是学习 UEFI 启动过程的笔记记录,主要内容源自 AI 搜索整理。


Seriousness is the cornerstone of everything!

posted @
2025-11-05 19:23 
阿源-  阅读(
78)  评论(
0)   
收藏 
举报

top 命令的load average和vmstat 的r列和b列的关系是什么?区别又是什么?

thbcm阅读(310)

top 命令的load average和vmstat 的r列和b列的关系是什么?区别又是什么?

最近在复习linux系统负载问题排查,主要涉及到cpu,内存,io,网络等指标的一些习惯的linux命令,发现top命令的load average指标和vmstat指标的r列以及b列似乎有些联系,但是又搞不懂具体是什么关系,又有什么区别,于是便上网查询整理了一下,以便日后学习工作中再次用到

# 一、vmstat 的 r 列 含义

vmstat 输出示例(关注前几列):

点击查看代码

procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in    cs us sy id wa st
 2  1  123456 123456 123456 123456   0    0    10    20  100    200 10  5 80  5  0

r(running or runnable):

表示处于运行态(running)或就绪态(runnable)的进程数,即正在使用 CPU 或等待 CPU 调度的进程数量。
不包括处于不可中断睡眠(D 状态)的进程。

b(blocked):

在较老版本的 vmstat 中,b 表示阻塞等待资源的进程数。
️ 但在现代 Linux(自 procps-ng 修订后)中,b 列实际上表示的是处于不可中断睡眠状态(TASK_UNINTERRUPTIBLE)的进程数,即通常所说的“D 状态”进程(如等待磁盘 I/O)。
注意:man vmstat 中对 b 的描述可能滞后,实际行为以内核和 procps 版本为准。现代 Linux 中,b ≈ D 状态进程数。

二、Load Average 的组成

Linux 的 Load Average(通过 /proc/loadavg 获取)定义为:
系统中 处于可运行状态(TASK_RUNNING) 或 不可中断睡眠状态(TASK_UNINTERRUPTIBLE) 的进程的指数加权移动平均数。

也就是说:

Load Average ≈ avg( runnable 进程数 + D状态进程数 )

这是 Linux 特有的设计(与传统 Unix 不同,后者通常只统计 runnable 进程)。

三、top 中的 Load Average

top 命令顶部显示的 load average(如 load average: 1.25, 0.98, 0.76)直接来自 /proc/loadavg,因此其含义与上述一致:
包含 runnable + D 状态进程。

四、关键对比:vmstat 的 r + b 是否等于 Load Average?

理论上:
r ≈ runnable 进程数(对应 TASK_RUNNING)
b ≈ D 状态进程数(TASK_UNINTERRUPTIBLE)
Load Average ≈ runnable + D 状态进程 的时间加权平均值
实际上:
r + b 是某一瞬间的快照值(瞬时值)
Load Average 是过去 1/5/15 分钟的平滑平均值(带指数衰减)

因此:
r + b 的瞬时值 ≠ Load Average 的数值
但 r + b 的长期趋势应与 Load Average 一致。如果 load 很高,通常 r + b 也会较大。
举例说明:

假设系统有 4 个 CPU 核心:
某时刻 vmstat 显示:r=3, b=2 → 瞬时活跃任务数 = 5
但过去 1 分钟大部分时间系统很空闲,只有偶尔 spike → 1 分钟 load average 可能只有 1.0

反之:
如果持续有 6 个任务(r=4, b=2)运行几分钟 → 1 分钟 load average 会逐渐接近 6

五、总结对比表

六、实用建议

若 Load Average 高但 r 很小、b 很大 → 很可能是 I/O 瓶颈(如慢磁盘、NFS 卡住)
若 Load Average 高且 r 很大 → CPU 资源不足,需优化代码或扩容
使用 ps aux awk ‘$8 ~ /^D/’ 查看 D 状态进程
使用 iostat -x 1 查看 I/O 利用率(%util)和等待时间(await)

结论:

综上vmstat 的 r 列不包含 D 状态进程,b 列在现代 Linux 中代表 D 状态进程数
Load Average = runnable + D 状态进程 的时间平均值
r + b 是瞬时值,Load Average 是历史平均值,二者概念相关但数值不等价

因此理解这一点,对精准定位系统瓶颈至关重要。

posted @
2025-11-05 21:19 
渡边灬  阅读(
37)  评论(
0)   
收藏 
举报

你的下一个 AI 编程搭档:AnyRouter 免费 Claude Code 4.5深度体验

thbcm阅读(285)

你的下一个 AI 编程搭档:AnyRouter 免费 Claude Code 4.5深度体验

引言:AI 编程的未来已来

​在快速发展的数字时代,人工智能正在重塑编程的边界。无论你是开发者、学生还是技术爱好者,高效的编程工具都能让你事半功倍。今天,我们要为你介绍一个颠覆性的平台——AnyRouter,一个让你免费体验顶级 AI 编程工具 Claude Code 的公益中转站!现在注册,还能立刻获得 $100 免费额度,机会稍纵即逝,快来加入吧!


AnyRouter 是什么?

AnyRouter 是一个致力于为全球开发者提供免费 Claude Code 服务的共享平台。Claude Code 作为 Anthropic 开发的顶级 AI 编程助手,以其强大的代码生成、调试和优化能力闻名。然而,获取 Claude Code 的高昂成本让许多开发者望而却步。AnyRouter 打破这一壁垒,通过中转服务让你无需付费即可畅享 Claude Code 的完整功能!

无需复杂配置,只需简单几步设置环境变量,你就能在自己的项目中使用 Claude Code,体验 AI 驱动的编程效率革命。现在,AnyRouter 还在推广期,注册即可获赠 $100 的 API 额度,邀请好友还能额外获得 $50/人,无上限!

核心优势:为什么选择 AnyRouter?

  1. 完全免费,零门槛体验 AnyRouter 的使命是让每一位开发者都能用上顶级的 AI 编程工具。无需信用卡,只需通过邀请链接注册,即可获得 $100 免费额度,轻松开启 Claude Code 的强大功能!
  2. 简单易用,快速上手 AnyRouter 的使用流程极简:安装 Node.js、设置两个环境变量(ANTHROPIC_AUTH_TOKENANTHROPIC_BASE_URL),即可在你的项目文件夹中运行 Claude Code。整个过程不超过 5 分钟,即使是编程新手也能轻松上手!
  3. 强大的 Claude Code 支持 Claude Code 能帮助你生成代码、调试错误、优化算法,甚至提供架构设计建议。无论是 Web 开发、数据分析还是机器学习,AnyRouter 让 Claude Code 成为你的得力助手。
  4. 推广期福利,邀请赚更多 通过 AnyRouter 的邀请机制,分享你的专属链接,每邀请一位好友注册,你和好友都能额外获得 $50 额度!更有“薅羊毛”小技巧:用备用邮箱自邀,轻松拿满 $100!
  5. 稳定可靠,社区驱动 AnyRouter 由技术社区支持,运行稳定,偶尔的小问题也能通过官方指南快速解决(如使用 VPN 或备用 endpoint)。这是一个真正的“赛博菩萨”项目,旨在让更多人受益于 AI 技术!

使用场景:AnyRouter 如何改变你的编程生活?

  • 学生​:正在学习 Python、JavaScript 或其他语言?AnyRouter 让你用 Claude Code 快速生成示例代码,理解复杂概念,提升学习效率。
  • 独立开发者​:独自开发 Web 应用或移动端项目?Claude Code 能帮你快速生成前端代码、优化后端逻辑,甚至调试复杂 bug,节省宝贵时间。
  • 团队协作​:在团队项目中,AnyRouter 的 Claude Code 能提供一致的代码风格建议,加速代码审查和迭代,助力团队高效协作。
  • 技术爱好者​:想尝试 AI 驱动的编程但预算有限?AnyRouter 让你零成本体验 Claude Code,探索 AI 在编程中的无限可能。

使用指南

1️⃣ 安装 Node.js(已安装可跳过)

确保 Node.js 版本 ≥ 18.0

Ubuntu / Debian 用户

Bash

curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo bash -
sudo apt-get install -y nodejs
node --version

macOS 用户

Bash

sudo xcode-select --install
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install node
node --version

2️⃣ 安装 Claude Code

Bash

npm install -g @anthropic-ai/claude-code
claude --version

3️⃣ 开始使用

  • 获取 Auth Token: ANTHROPIC_AUTH_TOKEN :注册后在 API 令牌 页面点击 添加令牌 获得(以 sk- 开头)
    • 名称随意,额度建议设为无限额度,其他保持默认设置即可
  • API 地址: ANTHROPIC_BASE_URLhttps://anyrouter.top 是本站的 API 服务地址,与主站地址相同

在您的项目目录下运行:

Bash

cd your-project-folder
export ANTHROPIC_AUTH_TOKEN=sk-... 
export ANTHROPIC_BASE_URL=https://anyrouter.top
claude

运行后:

  1. 选择你喜欢的主题 + Enter
  2. 确认安全须知 + Enter
  3. 使用默认 Terminal 配置 + Enter
  4. 信任工作目录 + Enter

开始在终端里和你的 AI 编程搭档一起写代码吧!

4️⃣ 配置环境变量(推荐)

为避免每次重复输入,可将环境变量写入 bash_profile 和 bashrc:

Bash

echo -e '\n export ANTHROPIC_AUTH_TOKEN=sk-...' >> ~/.bash_profile
echo -e '\n export ANTHROPIC_BASE_URL=https://anyrouter.top' >> ~/.bash_profile
echo -e '\n export ANTHROPIC_AUTH_TOKEN=sk-...' >> ~/.bashrc
echo -e '\n export ANTHROPIC_BASE_URL=https://anyrouter.top' >> ~/.bashrc
echo -e '\n export ANTHROPIC_AUTH_TOKEN=sk-...' >> ~/.zshrc
echo -e '\n export ANTHROPIC_BASE_URL=https://anyrouter.top' >> ~/.zshrc

重启终端后,直接使用:

Bash

cd your-project-folder
claude

即可使用 Claude Code


开启 Claude Code 之旅

AnyRouter 是你通往 AI 编程世界的钥匙。现在正值推广期,免费 $100 额度限时发放,错过再等一年!只需简单几步:

  1. 访问 AnyRouter 官网 ,通过邀请链接注册。
  2. 按照官网指南安装 Node.js 并设置环境变量。
  3. 在项目中运行 Claude Code,体验 AI 编程的丝滑快感!

posted @
2025-11-05 22:09 
yangshangwei  阅读(
188)  评论(
1)   
收藏 
举报

ASP.NET Core Blazor 核心功能三:Blazor与JavaScript互操作——让Web开发更灵活

thbcm阅读(268)

ASP.NET Core Blazor 核心功能三:Blazor与JavaScript互操作——让Web开发更灵活

嗨,大家好!我是码农刚子。今天我们来聊聊Blazor中C#与JavaScript互操作。我知道很多同学在听到”Blazor”和”JavaScript”要一起工作时会有点懵,但别担心,我会用最简单的方式带你掌握这个技能!

为什么要学JavaScript互操作?

想象一下:你正在用Blazor开发一个超棒的应用,但突然需要用到某个只有JavaScript才能实现的炫酷效果,或者要集成一个超好用的第三方JS库。这时候,JavaScript互操作就是你的救星!

简单来说,它让Blazor和JavaScript可以”握手合作”,各展所长。下面我们就从最基础的部分开始。

1. IJSRuntime – 你的JavaScript通行证

在Blazor中,IJSRuntime是与JavaScript沟通的桥梁。获取它超级简单:

@inject IJSRuntime JSRuntime

<button @onclick="ShowAlert">点我弹窗!</button>
@code {
    private async Task ShowAlert()
    {
        await JSRuntime.InvokeVoidAsync("alert", "Hello from Blazor!");
    }
}

就两行关键代码:

  • @inject IJSRuntime JSRuntime – 拿到通行证
  • InvokeVoidAsync – 调用不返回值的JS函数

实际场景:比如用户完成某个操作后,你想显示一个提示,用这种方式就特别方便。

2. 调用JavaScript函数 – 不只是简单弹窗

当然,我们不会只满足于弹窗。来看看更实用的例子:

首先,在wwwroot/index.html中添加我们的JavaScript工具函数:

<script>
    // 创建命名空间避免全局污染
    window.myJsHelpers = {
        showNotification: function (message, type) {
            // 模拟显示一个漂亮的提示框
            const notification = document.createElement('div');
            notification.style.cssText = `
                position: fixed;
                top: 20px;
                right: 20px;
                padding: 15px 20px;
                border-radius: 5px;
                color: white;
                z-index: 1000;
                transition: all 0.3s ease;
            `;
            
            if (type === 'success') {
                notification.style.backgroundColor = '#28a745';
            } else if (type === 'error') {
                notification.style.backgroundColor = '#dc3545';
            } else {
                notification.style.backgroundColor = '#17a2b8';
            }
            
            notification.textContent = message;
            document.body.appendChild(notification);
            
            // 3秒后自动消失
            setTimeout(() => {
                notification.remove();
            }, 3000);
        },
        
        getBrowserInfo: function () {
            return {
                userAgent: navigator.userAgent,
                language: navigator.language,
                platform: navigator.platform
            };
        },
        
        // 带参数的计算函数
        calculateDiscount: function (originalPrice, discountPercent) {
            return originalPrice * (1 - discountPercent / 100);
        }
    };
</script>

然后在Blazor组件中使用:

@inject IJSRuntime JSRuntime

<div class="demo-container">
    <h3>JavaScript函数调用演示</h3>
    
    <button @onclick="ShowSuccessNotification" class="btn btn-success">
        显示成功提示
    </button>
    
    <button @onclick="ShowErrorNotification" class="btn btn-danger">
        显示错误提示
    </button>
    
    <button @onclick="GetBrowserInfo" class="btn btn-info">
        获取浏览器信息
    </button>
    
    <button @onclick="CalculatePrice" class="btn btn-warning">
        计算折扣价格
    </button>
    @if (!string.IsNullOrEmpty(browserInfo))
    {
        <div class="alert alert-info mt-3">
            <strong>浏览器信息:</strong> @browserInfo
        </div>
    }

    @if (discountResult > 0)
    {
        <div class="alert alert-success mt-3">
            <strong>折扣价格:</strong> ¥@discountResult
        </div>
    }
</div>
@code {
    private string browserInfo = "";
    private decimal discountResult;

    private async Task ShowSuccessNotification()
    {
        await JSRuntime.InvokeVoidAsync("myJsHelpers.showNotification", 
            "操作成功!数据已保存。", "success");
    }

    private async Task ShowErrorNotification()
    {
        await JSRuntime.InvokeVoidAsync("myJsHelpers.showNotification", 
            "出错了!请检查网络连接。", "error");
    }

    private async Task GetBrowserInfo()
    {
        var info = await JSRuntime.InvokeAsync<BrowserInfo>("myJsHelpers.getBrowserInfo");
        browserInfo = $"语言: {info.Language}, 平台: {info.Platform}";
    }

    private async Task CalculatePrice()
    {
        discountResult = await JSRuntime.InvokeAsync<decimal>(
            "myJsHelpers.calculateDiscount", 1000, 20); // 原价1000,8折
    }

    // 定义接收复杂对象的类
    private class BrowserInfo
    {
        public string UserAgent { get; set; }
        public string Language { get; set; }
        public string Platform { get; set; }
    }
}

Note

  • 使用InvokeVoidAsync调用不返回值的函数
  • 使用InvokeAsync<T>调用有返回值的函数,记得指定返回类型
  • 复杂对象会自动序列化/反序列化

3. 把.NET方法暴露给JavaScript – 双向操作

有时候,我们也需要让JavaScript能调用我们的C#方法。这就用到[JSInvokable]特性了。

@inject IJSRuntime JSRuntime
@implements IDisposable

<div class="demo-container">
    <h3>.NET方法暴露演示</h3>
    
    <div class="mb-3">
        <label>消息内容:</label>
        <input @bind="message" class="form-control" />
    </div>
    
    <div class="mb-3">
        <label>重复次数:</label>
        <input type="number" @bind="repeatCount" class="form-control" />
    </div>
    
    <button @onclick="RegisterDotNetMethods" class="btn btn-primary">
        注册.NET方法给JavaScript使用
    </button>
    
    <div id="js-output" class="mt-3 p-3 border rounded">
        <!-- JavaScript会在这里输出内容 -->
    </div>
</div>
@code {
    private string message = "Hello from .NET!";
    private int repeatCount = 3;
    private DotNetObjectReference<MyComponent> dotNetHelper;

    protected override void OnInitialized()
    {
        dotNetHelper = DotNetObjectReference.Create(this);
    }

    private async Task RegisterDotNetMethods()
    {
        await JSRuntime.InvokeVoidAsync("registerDotNetHelper", dotNetHelper);
    }

    [JSInvokable]
    public string GetFormattedMessage()
    {
        return string.Join(" ", Enumerable.Repeat(message, repeatCount));
    }

    [JSInvokable]
    public async Task<string> ProcessDataAsync(string input)
    {
        // 模拟一些异步处理
        await Task.Delay(500);
        return $"处理后的数据: {input.ToUpper()} (处理时间: {DateTime.Now:HH:mm:ss})";
    }

    [JSInvokable]
    public void ShowAlert(string alertMessage)
    {
        // 这个方法会被JavaScript调用
        // 在实际应用中,你可能会更新组件状态或触发其他操作
        Console.WriteLine($"收到JavaScript的警告: {alertMessage}");
    }

    public void Dispose()
    {
        dotNetHelper?.Dispose();
    }
}

对应的JavaScript代码:

// 在index.html中添加
function registerDotNetHelper(dotNetHelper) {
    // 存储.NET引用供后续使用
    window.dotNetHelper = dotNetHelper;
    
    // 演示调用.NET方法
    callDotNetMethods();
}

async function callDotNetMethods() {
    if (!window.dotNetHelper) {
        console.error('.NET helper 未注册');
        return;
    }
    
    try {
        // 调用无参数的.NET方法
        const message = await window.dotNetHelper.invokeMethodAsync('GetFormattedMessage');
        
        // 调用带参数的异步.NET方法
        const processed = await window.dotNetHelper.invokeMethodAsync('ProcessDataAsync', 'hello world');
        
        // 调用void方法
        window.dotNetHelper.invokeMethodAsync('ShowAlert', '这是从JS发来的消息!');
        
        // 在页面上显示结果
        const output = document.getElementById('js-output');
        output.innerHTML = `
            <strong>来自.NET的消息:</strong> ${message}<br>
            <strong>处理后的数据:</strong> ${processed}
        `;
        
    } catch (error) {
        console.error('调用.NET方法失败:', error);
    }
}

Note

  • 记得使用DotNetObjectReference来创建引用
  • 使用Dispose()及时清理资源
  • 异步方法要返回TaskTask<T>

4. 使用JavaScript库 – 集成第三方神器

这是最实用的部分!让我们看看如何集成流行的JavaScript库。

示例:集成Chart.js图表库

首先引入Chart.js:

<!-- 在index.html中 -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

创建图表辅助函数:

// 在index.html中或单独的JS文件
window.chartHelpers = {
    createChart: function (canvasId, config) {
        const ctx = document.getElementById(canvasId).getContext('2d');
        return new Chart(ctx, config);
    },
    
    updateChart: function (chart, data) {
        chart.data = data;
        chart.update();
    },
    
    destroyChart: function (chart) {
        chart.destroy();
    }
};

Blazor组件:

@inject IJSRuntime JSRuntime
@implements IDisposable

<div class="chart-demo">
    <h3>销售数据图表</h3>
    <canvas id="salesChart" width="400" height="200"></canvas>
    
    <div class="mt-3">
        <button @onclick="LoadSalesData" class="btn btn-primary">加载销售数据</button>
        <button @onclick="SwitchToProfitChart" class="btn btn-secondary">切换到利润图表</button>
    </div>
</div>
@code {
    private IJSObjectReference chartInstance;
    private bool isSalesData = true;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await InitializeChart();
        }
    }

    private async Task InitializeChart()
    {
        var config = new
        {
            type = "bar",
            data = new
            {
                labels = new[] { "一月", "二月", "三月", "四月", "五月" },
                datasets = new[]
                {
                    new
                    {
                        label = "销售额",
                        data = new[] { 65, 59, 80, 81, 56 },
                        backgroundColor = "rgba(54, 162, 235, 0.5)",
                        borderColor = "rgba(54, 162, 235, 1)",
                        borderWidth = 1
                    }
                }
            },
            options = new
            {
                responsive = true,
                plugins = new
                {
                    title = new
                    {
                        display = true,
                        text = "月度销售数据"
                    }
                }
            }
        };

        chartInstance = await JSRuntime.InvokeAsync<IJSObjectReference>(
            "chartHelpers.createChart", "salesChart", config);
    }

    private async Task LoadSalesData()
    {
        var newData = new
        {
            labels = new[] { "一月", "二月", "三月", "四月", "五月", "六月" },
            datasets = new[]
            {
                new
                {
                    label = "销售额",
                    data = new[] { 65, 59, 80, 81, 56, 75 },
                    backgroundColor = "rgba(54, 162, 235, 0.5)"
                }
            }
        };

        await JSRuntime.InvokeVoidAsync("chartHelpers.updateChart", 
            chartInstance, newData);
    }

    private async Task SwitchToProfitChart()
    {
        isSalesData = !isSalesData;
        
        var newData = isSalesData ? 
            new { 
                labels = new[] { "Q1", "Q2", "Q3", "Q4" },
                datasets = new[] {
                    new {
                        label = "销售额",
                        data = new[] { 100, 120, 110, 130 },
                        backgroundColor = "rgba(54, 162, 235, 0.5)"
                    }
                }
            } :
            new {
                labels = new[] { "Q1", "Q2", "Q3", "Q4" },
                datasets = new[] {
                    new {
                        label = "利润",
                        data = new[] { 30, 45, 35, 50 },
                        backgroundColor = "rgba(75, 192, 192, 0.5)"
                    }
                }
            };

        await JSRuntime.InvokeVoidAsync("chartHelpers.updateChart", 
            chartInstance, newData);
    }

    public async void Dispose()
    {
        if (chartInstance != null)
        {
            await JSRuntime.InvokeVoidAsync("chartHelpers.destroyChart", chartInstance);
        }
    }
}

常见问题与解决方案

问题1:JS互操作调用失败

症状:控制台报错,函数未定义

解决

try 
{
    await JSRuntime.InvokeVoidAsync("someFunction");
}
catch (JSException ex)
{
    Console.WriteLine($"JS调用失败: {ex.Message}");
    // 回退方案
    await JSRuntime.InvokeVoidAsync("console.warn", "功能不可用");
}

问题2:性能优化

对于频繁调用的JS函数,可以使用IJSInProcessRuntime

@inject IJSRuntime JSRuntime

@code {
    private IJSInProcessRuntime jsInProcess;

    protected override void OnInitialized()
    {
        jsInProcess = (IJSInProcessRuntime)JSRuntime;
    }

    private void HandleInput(ChangeEventArgs e)
    {
        // 同步调用,更高效
        jsInProcess.InvokeVoidAsync("handleInput", e.Value.ToString());
    }
}

问题3:组件销毁时资源清理

@implements IDisposable

@code {
    private DotNetObjectReference<MyComponent> dotNetRef;
    private IJSObjectReference jsModule;

    protected override async Task OnInitializedAsync()
    {
        dotNetRef = DotNetObjectReference.Create(this);
        jsModule = await JSRuntime.InvokeAsync<IJSObjectReference>(
            "import", "./js/myModule.js");
    }

    public async void Dispose()
    {
        dotNetRef?.Dispose();
        
        if (jsModule != null)
        {
            await jsModule.DisposeAsync();
        }
    }
}

Blazor的JavaScript互操作其实没那么难的。记住这几个关键点:

  • IJSRuntime 是你的通行证
  • InvokeVoidAsyncInvokeAsync 是主要工具
  • [JSInvokable] 让.NET方法对JS可见
  • 及时清理资源 很重要

现在你已经掌握了Blazor与JavaScript互操作的核心技能!试着在自己的项目中实践一下,示例源码更放在仓库:https://github.com/shenchuanchao/BlazorApp/tree/master/BlazorAppWasm/Pages

​以上就是《ASP.NET Core Blazor 核心功能二:Blazor与JavaScript互操作——让Web开发更灵活》的全部内容,希望你有所收获。关注、点赞,持续分享 posted @
2025-11-05 21:34 
码农刚子  阅读(
149)  评论(
0)   
收藏 
举报

【Kubernetes】入门-部署Spring应用

thbcm阅读(303)

【Kubernetes】入门-部署Spring应用

Kubernetes

基本概念

Pod

是什么: Kubernetes 中最小的、最简单的部署和管理单元。

类比: 一台“逻辑主机”或一个“虚拟机实例”。

关键点

  • 一个 Pod 可以包含一个或多个紧密相关的容器(例如,一个应用容器和一个日志收集 sidecar 容器)。
  • 这些容器共享网络命名空间(拥有相同的 IP 地址和端口空间)、存储卷和其他资源。
  • Pod 是短暂的,会被频繁地创建和销毁

Deployment

是什么: 用于部署和管理 Pod 副本集的声明式定义。

解决的问题: 如何确保指定数量的、完全相同的 Pod 副本始终在运行?如何无缝更新应用(滚动更新)?如何在更新出问题时快速回滚?

关键点

  • 你定义一个“期望状态”(例如,需要运行 3 个 Nginx Pod),Deployment 控制器会确保实际状态始终匹配这个期望状态。

  • 它是实现无状态应用部署的首选控制器。

Service

是什么: 一个稳定的网络端点,用于访问一组 Pod。

解决的问题: Pod 是短暂的,IP 地址会变。客户端如何稳定地找到并访问这些动态变化的 Pod?

关键点

  • Service 提供一个固定的 虚拟 IP (ClusterIP) 和 DNS 名称。

  • 通过 标签选择器 (Label Selector) 来匹配一组 Pod,并将流量负载均衡到这些 Pod 上。

  • 常见的类型:

    • ClusterIP: 默认类型,仅在集群内部可访问。

    • NodePort: 在每个节点上开放一个静态端口,从而可以从集群外部访问。

    • LoadBalancer: 使用云提供商的负载均衡器,向外部公开服务

Volume (存储卷)

是什么: 允许 Pod 中的容器持久化存储数据。

解决的问题: 容器内部的文件系统是临时的,容器重启后数据会丢失。如何持久化保存数据(如数据库文件、日志)?

关键点

  • 存储卷的生命周期独立于 Pod。即使 Pod 被销毁,存储卷中的数据依然存在。

  • 支持多种后端存储类型:本地存储、云存储(如 AWS EBS、GCE PD)、NFS、分布式存储(如 Ceph)等。

Namespace (命名空间)

是什么: 在物理集群内部创建的虚拟集群,用于实现资源隔离和分组。

解决的问题: 在有多个团队或项目的大型集群中,如何避免资源(如 Pod、Service 名称)冲突?如何实现资源配额管理?

关键点

  • 默认有 default, kube-system (系统组件), kube-public 等命名空间。

  • 可以为每个命名空间设置资源配额,限制其能使用的 CPU、内存等资源总量。

ConfigMap & Secret

  • ConfigMap: 用于将非机密的配置数据(如配置文件、环境变量、命令行参数)与容器镜像解耦。让你可以不重写镜像就能改变应用配置。

  • Secret: 与 ConfigMap 类似,但专门用于存储敏感信息,如密码、OAuth 令牌、SSH 密钥。数据会以 Base64 编码(非加密,需额外措施保障安全)存储。

使用K8S部署Spring Boot应用

前置条件,docker和kubernetes已安装配置

参考:macOS上优雅运行Docker容器

准备一个Spring Boot应用

SpringK8sDemoApplication

@SpringBootApplication
@RestController
public class SpringK8sDemoApplication {

	private static Date firstTime;
	private static SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

	public static void main(String[] args) {
		SpringApplication.run(SpringK8sDemoApplication.class, args);
	}

	@GetMapping("/")
	public String hello() {
		if (firstTime == null) {
			firstTime = new Date();
		}
		// 容器里看日志是否有负载均衡
		System.out.println("request in " + formatter.format(new Date()));
		return "Hello from Spring Boot on Kubernetes! first time: " + formatter.format(firstTime);
	}

	@GetMapping("/health")
	public String health() {
		return "OK";
	}
}

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.5.7</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>spring-k8s-demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>spring-k8s-demo</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>21</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

构建 Docker 镜像

Dockerfile

FROM eclipse-temurin:21-jre-alpine AS runtime

# 创建应用目录
WORKDIR /app

# 复制jar文件
COPY target/spring-k8s-demo-0.0.1-SNAPSHOT.jar app.jar

# 暴露端口
EXPOSE 8080

# 启动应用
ENTRYPOINT ["java", "-jar", "app.jar"]

build.sh

# 切换指定JDK版本(可选)
export JAVA_HOME=`/usr/libexec/java_home -v 21`

# 可替换mvn
mvnd clean package -DskipTests

# 构建镜像
docker build -t spring-k8s-demo:latest .

编写 k8s Deployment 文件

k8s-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: spring-app
spec:
  replicas: 2  # 运行 2 个 Pod 副本
  selector:
    matchLabels:
      app: spring-app
  template:
    metadata:
      labels:
        app: spring-app
    spec:
      containers:
      - name: spring-app
        image: spring-k8s-demo:latest
        ports:
        - containerPort: 8080   # 容器内暴露的端口
        livenessProbe:          # 存活探针
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 300
          periodSeconds: 10
        readinessProbe:         # 就绪探针
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 300
          periodSeconds: 10
        resources:              # 分配资源
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"

k8s-services.yaml

# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: sprint-service
  labels:
    app: sprint-service
spec:
  type: NodePort
  selector:
    app: spring-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
      nodePort: 30080

配置文件解释

  • Deployment:

    • replicas: 2:确保任何时候都有 2 个 Pod 在运行。
    • selector & labels:Deployment 通过 app: spring-app 这个标签来管理它创建的 Pod。
    • image:指定从哪个镜像运行容器。
    • resources:为容器设置 CPU 和内存的资源请求与限制,这是生产环境的最佳实践。
  • Service:

    • type: NodePort: 在每个节点上开放一个静态端口,从而可以从集群外部访问
    • selector: app: spring-app:Service 通过这个标签找到要代理的 Pod。
    • ports:将 Service 的 80 端口流量转发到 Pod 的 8080 端口。

部署到集群

应用配置

kubectl apply -f k8s-deployment.yaml
kubectl apply -f k8s-services.yaml

测试

  1. 查看pod状态
kubectl get pods

Pod 状态变为 Running

  1. 查看Deployment状态
kubectl get deployment
  1. 查看 Service
kubectl get service
  1. curl
curl http://192.168.64.3:30080/

根据时间,可以看到每次轮询到不同pod,也就是不同容器

注意:因为我使用是colima所以需要使用colima list命令查出来IP是192.168.64.3

k8s 常用命令

# 查看所有节点
kubectl get nodes

# 查看节点详情
kubectl describe node <node-name>

# 查看Pod
kubectl get pods

# 查看Pod详情
kubectl describe pod <pod-name>

# 查看Pod日志
kubectl logs <pod-name> -n <namespace>

# 查看单个 Pod 的日志
kubectl logs <pod-name>

# 查看部署
kubectl get deployments

# 查看部署详情
kubectl describe deployment <deployment-name> 

# 查看由 Deployment 创建的所有 Pod 的日志
kubectl logs -l app=spring-app

# 转发端口(用于本地测试)
kubectl port-forward svc/sprint-service 8080:80

# 水平扩缩容(将副本数从 2 增加到 3)
kubectl scale deployment spring-app --replicas=3

# 查看部署历史
kubectl rollout history deployment/<deployment-name>

# 滚动更新(例如,更新到新版本的镜像)
kubectl set image deployment/spring-app java-app=spring-k8s-demo:2.0.0

# 回滚到上一个版本
kubectl rollout undo deployment/<deployment-name>

# 删除部署
kubectl delete -f k8s-deployment.yaml
kubectl delete -f k8s-services.yaml

引用

https://github.com/WilsonPan/java-developer

例子: https://github.com/WilsonPan/java-developer/tree/main/samples/spring-demo

posted @
2025-11-06 00:03 
WilsonPan  阅读(
20)  评论(
0)   
收藏 
举报



刷新页面
返回顶部

让 AI 记住我家狗叫「十六」,原来只需要 5 分钟

thbcm阅读(268)

让 AI 记住我家狗叫「十六」,原来只需要 5 分钟

现在用 AI,最怕的不是它不会,而是它不记得

比如,我家的狗叫「十六」。

前一天,我刚手把手(打字)教会它“十六是我家狗”,还分享了三小时宠物日常。过两天打开新对话,我兴冲冲地发了句:“十六昨天终于学会游泳了!”结果,它用那种最标准的 AI 腔调回我:“很抱歉,您提到的「十六」是指什么?”

兄弟们,那一刻我的血压“噌”就上来了。这就是现实版的“赛博背叛”啊?

一、AI 为啥总是“金鱼记忆”?

虽然如今的 AI 已具备短期记忆,却难以真正理解用户——这几乎是当下所有 AI 用户的核心痛点。其根本原因,正是模型缺乏长期记忆能力。

这也是为什么业界都在卷记忆:从 OpenAI 的长期记忆实验,到各类 Memory Agent 框架,大家都在试图解决如何让模型记住用户这件事。

要理解这个问题,我们得从模型的记忆架构说起。传统 LLM 的记忆机制主要分两类:

  • 参数记忆(Parametric Memory):“焊”在模型权重里的知识,更新极其困难。
  • 激活记忆(Activation Memory):对话时临时的上下文缓存,纯“日抛”记忆,会话结束当场失忆。

这意味着,模型在生成回答时,所能“看到”的信息完全取决于上下文窗口的大小。当上下文过长时,计算量将以平方级增长。为了控制计算成本,模型会自动遗忘较早的内容。于是,LLM 能和你聊逻辑,它却记不住你家狗的名字。能总结论文,却记不住你昨天说过的喜好。

更为严重的问题是幻觉——当模型缺乏连续上下文时,会自动补全缺失的信息,从而生成看似合理但错误的回答。

过去大家用 RAG(检索增强生成)或外部数据库来缓解这个问题,但那些方案大多是临时记忆:能查能存,却不懂何时调取、何时更新,更谈不上长期记忆。

二、让记忆成为一种系统资源:MemOS 登场

直到前段时间,我在 GitHub 上刷到一个开源项目——MemOS 专为 LLM 设计的长期记忆系统。

GitHub 地址:github.com/MemTensor/MemOS

在了解 MemOS 之前,我一直认为 AI 记忆系统只是一堆“补丁”,开发者不得不反复折腾嵌入、检索和更新等繁琐的逻辑。而 MemOS 的理念,是把“记忆”抽象为系统级能力——你只需告诉它要记住的内容,其余都由系统自动处理。

这听起来就像人脑的记忆机制:我们不是刻意存储信息,而是让重要的东西自然被记住。MemOS 就是在让 AI 拥有这种类人式的长期记忆能力

三、记忆系统,如何“自己思考”?

我扒了扒它的文档,发现 MemOS 的记忆架构设计确实有点“东西”。

它并非简单地将聊天记录存入数据库,而是在每一条记忆之间建立关联——类似维基百科的超链接。它参考了操作系统的分层思想,将记忆系统划分为三层。

当我和 AI 聊到“十六”,系统不只是存下“十六 = 狗”这条信息,而是还会同时捕捉到对话的时间、情绪、上下文主题,并自动在它们之间建立语义链接。这样,下次你提“带十六去看兽医”,AI 就能主动推断出「十六」是那只宠物,而不是把它当作一个数字。

换句话说,MemOS 是让 AI “长记性”的操作系统。它不仅存数据,还能在推理中动态调度、复用和更新这些记忆。

更智能的是,系统会自动修剪和优化这张记忆图谱。当某些信息(比如「十六」)被频繁使用时,MemOS 会提高其优先级;反之则逐渐淡化。久而久之,系统会形成一张动态演化的记忆图谱,不断重塑、筛选、连接。

这意味着,AI 不再是死记硬背的机器,而是会“取舍”的伙伴。

四、从 Prompt 到 Context:记忆驱动的上下文工程

有了记性,还得会“用”才行。这就引出了上下文工程 这个核心技术。

  • 传统 Prompt 工程:依赖“人工填鸭”,手动拼接提示词模板。

  • MemOS 模式:Prompt 不再是一次性的提示词输入,而是一个动态上下文重构的过程。

模型每次生成前,MemOS 会根据任务意图自动“三连”:

  1. 检索: 从记忆库中查出相关内容;
  2. 评估: 评估上下文权重;
  3. 注入: 将结果组织成结构化上下文并注入模型输入。

比如向 AI 提问:帮我查查上次说的那个”Redis 优化方案”。MemOS 会立刻自动找到历史记录,组合为上下文。

[2024-10-10] Redis 优化建议: 
1. 使用 pipeline 批量命令;
2. 调整内存回收策略;
3. 添加监控指标。

再交给模型生成回答。这不仅极大减少了幻觉概率,也让推理更加稳健,具备了记忆连续性。

五、三步让 AI 记住「十六」

MemOS 已经为 AI 应用开发的开发者,提供了完善的 API 与 SDK,并且可以无缝集成主流智能体框架(如 Dify、LangGraph、Coze 等)。

以 Dify 为例,接入流程非常简单:

1.配置节点
在 Dify 的 ChatFlow 中添加两个 HTTP 节点:

  • /product/search:检索历史记忆
  • /product/add:写入新记忆

2.连接逻辑节点

  • search 的输出作为模型输入前的“上下文扩展”
  • 将模型的生成结果通过 add 节点写回 MemOS

3.示例代码

# 添加记忆
curl -X POST "http://127.0.0.1:8002/product/add" \
-H "Content-Type: application/json" \
-d '{
  "messages": [
    {"role": "user", "content": "十六是我家的狗"},
    {"role": "assistant", "content": "明白了,十六是你的狗狗!"}
  ],
  "mem_cube_id": "user_001"
}'

# 检索记忆
curl -X POST "http://127.0.0.1:8002/product/search" \
-H "Content-Type: application/json" \
-d '{
  "mem_cube_id":"user_001",
  "query":"十六是谁",
  "top_k":3
}'

几行代码,就能让 Dify 应用真正“记住”我家的狗「十六」!然后我还测试了一下,让它记住「十六」的生日,然后一周后再问它:“下周有什么要记得的事吗?”

它回答:“「十六」的生日要到了,别忘了准备小蛋糕。”

老实说,那一刻,我真有点被“治愈”到了。因为这不只是记忆被保存了,而是语境、情感和关系被理解并延续了

这就是记忆的力量,让 AI 从一个工具变成了伙伴

六、写在最后

人们常说“大模型的能力边界在模型之外”,而记忆,特别是模型长期记忆,就是让 AI 突破边界的关键一步

我认为,AI 不缺知识,也不缺算力,真正缺的是「连续性」——一种能跨越会话、理解上下文、记住人和关系的能力。

这,正是 MemOS 这样的 AI 记忆系统试图重塑的核心方向。

GitHub 地址:github.com/MemTensor/MemOS

或许在不久的未来,我们不会再说「和 AI 对话」,而是说「与一个真正记得你的智能体交流」。

因为到那时,AI 就不会再忘记 「十六」 的名字了。

作者:削微寒
扫描左侧的二维码可以联系到我

本作品采用署名-非商业性使用-禁止演绎 4.0 国际 进行许可。

posted @
2025-11-06 08:18 
削微寒  阅读(
273)  评论(
3)   
收藏 
举报

AI提效这么多,为什么不试试自己开发N个产品呢?

thbcm阅读(262)

AI提效这么多,为什么不试试自己开发N个产品呢?

前言

在这个AI恐怖的提效时代,很多同学应该都冒出过这样的想法:在AI的帮助下,这么快就做一个产品,为什么不尝试在业余时间自己做一堆产品,反正成本就只有自己的时间。万一有一个产品成功了,可能就不需要打工了。

加入欧阳的AI交流群

具像化需求从哪里来?

思考一下在以前,一个模糊的产品想法是如何落地为具体的产品呢?

  • 某天灵感一现有个模糊的产品idea
  • 找一个产品角色将这个模糊的产品idea具象为具体的产品需求
  • 找一个程序员将这些具体的产品需求开发出来

在以前的流程中有两个主要的角色:产品经理程序员

“一个模糊的产品idea”和“将具体的产品需求开发出来”这两个任务我们程序员都能完成,但是将模糊的产品idea具象为具体的产品需求,应该是绝大部分的程序员都做不到的。

既然我们程序员做不到,那么可以利用AI将模糊的产品idea具象为具体的产品需求吗?

答案是:可以的。

AI具象化产品需求

可以将你的模糊想法丢给AI,然后和AI进行需求讨论,我一般是这样做的:

  • 把我的模糊产品idea告诉AI
  • 和AI讨论出有哪些页面,这些页面的大概功能有哪些
  • 再来讨论每个页面里面的具体功能

这里有几个痛点:

  • AI的输出是纯文字的,太抽象了,只能通过脑子去想象AI描述的画面。如果想要看到这些UI,那么我就只有通过coding去将这些页面实现出来,但是经常实现出来后发现这并不是我想要的,导致很多次无效的coding。
  • 产品涉及到很多页面,和AI进行文字沟通产品的流程经常理解都不清楚。期望有个可视化的工作流工具直接“拖拖改改”就更直观了

欧阳最近发现一个AI工具uxbot,可以完美的解决我的这些痛点。

codex是将具体需求落地为产品的AI工具,uxbot是将模糊idea落地为具体需求的AI工具。

这个是uxbot的链接:https://www.uxbot.cn?utm_source=39cfdca6ed4c60d1&utm_medium=influencer&utm_influencer=f7fb1e9e05cf46f0

想做一个语音克隆的产品

从模糊idea到落地产品,AI工具已经可以贯穿全流程了。

比如我想做一个“语音克隆的产品”,我就只有这么一个模糊的idea,具体怎么做我也不知道。

把这个idea告诉给uxbot

1分钟后,他直接就给我生成了一个可视化的产品工作流:

这一步他直接就帮我生成了有哪些页面,这些页面的页面流转是什么样的,以及这些页面的功能是什么样的。

生成的这些页面以及页面的功能点看着还不错。

页面的功能如果不满意,直接点击卡片就可以修改页面的功能。

对页面的跳转不满意,直接通过拖拽就可以将改变页面的跳转。

想要新增页面也可以点击按钮新增一个页面就行。

理清楚了这个产品有哪些页面,以及这些页面大概有哪些功能之后,接下来就需要看看这些页面到底长什么样。

点击生成产品页面按钮就可以生成这些产品页面的预览了:

在生成页面时可以选择页面的风格,然后下方输入框就会生成风格对应的内置提示词,你也可以在这些提示词的基础上进行修改。

点击生成产品页面按钮这次就会生成最后的产品预览页面,这一步几分钟就完成了:

已经帮我生成了所有预览页面,并且这些页面就像是真实页面一样也是可以点击进行页面跳转的。

如果你对某个页面不满意,重新给他一个新的页面描述,然后点击重新生成按钮就可以生成新的页面。

还有一个AI助手功能也挺有用的,直接给他一个指令就可以让他帮你对页面进行微调,比如我这里想将“情感模拟”模块的背景色改为蓝色,直接给他说就行了:

如果你想手动微调页面也可以,选中你想修改的部分,直接改就行了,整个页面还支持可视化编辑的功能。

比如我想修改“声音的边界,从此被改写”这个文案和颜色,直接选中这个文案就可以修改了:

除了可以修改选中内容的样式,还可以设计交互,基本就是可视化编程了。

还有一个更好用的功能就是“配图”,比如生成的页面有这样的一个模块:

点击右边的“为当前界面配图”按钮,等待几分钟就帮我连页面的图都生成好了:

最后就是点击右上角的 “导出” 按钮将其导出:

这里可以选择hmtl或者Sketch进行导出,我选择的是html。

最后一步就是回到AI编辑器中,我这里是Codex:

这一步我们将uxbot生成的html文件丢改Codex,让他将这些html代码转换为nextjs代码就行了。

总结

uxbot + Codex这两个AI工具覆盖了从模糊想法到具体产品落地的全流程,根据uxbot团队透露他们最近要支持直接导出前端代码,这样连codex都可以省去了。最后附上uxbot的链接:https://www.uxbot.cn?utm_source=39cfdca6ed4c60d1&utm_medium=influencer&utm_influencer=f7fb1e9e05cf46f0

posted @
2025-11-06 08:24 
前端欧阳  阅读(
307)  评论(
1)   
收藏 
举报

关于AI编程能力对程序员的职业影响

thbcm阅读(309)

关于AI编程能力对程序员的职业影响

关于AI编程能力对程序员的职业影响

如今对于AI的很多媒体炒作(包括自媒体),在技术方面是有根本性错误的,例如,最近经常有人在嘲笑“老一辈说洗衣机不如手洗,这一辈说AI编程不如手写代码”之类的。这种行为的麻烦之处在于他们对问题本身都描述得不清楚,还要再加上几个自以为是的,不伦不类的比喻,根本是在搞乱一个旧问题的同时,制造一个新问题(制造不必要的对立)。在Twitter/X或微博上,我们确实经常会看到有人在不懂装懂嘲笑别人不了解人工神经网络,然后自己没有半个推文在科普这方面知识的,基本上是在炒作概念。贩卖焦虑。

那么在技术方面上,我们该如何分辨一个人所说的话是在炒作概念还是科普技术呢?较为简单的方法是:

  • 如果这个人说话,是在让你热血沸腾,很激动或很焦虑,不是想创业就是想放弃,wow,xxx时代来了,xxx要过时了,xxx要被淘汰了,这大概率是在炒作。
  • 如果这个人说话,能让你顺着他的用词一个一个查,且越查越知道自己要学啥,他通常是真的懂这项技术,并且在做科普。

另外,如果有人跟你说,他常常和一堆大牛线下聊AI技术。大概率情况不外乎两种:一,这堆人中只有一个大牛,他讲课来的。二,这堆人中至少有一个投资,其他人是创业找钱来的。一堆技术大牛在一起,聊啥都比聊技术可能性大。说真的,技术就不该让人热血沸腾。

对于文科生或对编程逻辑不太熟悉的人来说,学点vibe coding,确实能在产品设计阶段更好地和开发沟通,减少沟通成本,这是值得鼓励的。但作为技术人员,整天折腾提示词和大模型客户端,就敢出来炒概念、贩卖焦虑,这就太离谱了。靠几句 prompt 就想代表“AI编程能力”,这不是创新,是偷懒。技术不是拿来表演的,别把不学无术当成新趋势。

具体到技术上来说,现在大家所说的AI,大多数都属于大语言模型(LLM),这基本上是一种以自然语言处理(NLP)技术为核心的产品。虽然这类产品如今得到了多模态的扩展,多模态的模型可能在某程度上整合了自然语言处理、计算机视觉及语音处理等方面,但它在编程领域所能做到的只是将自然语言”翻译”成编程语言,而不是直接编译程序或进行解释执行。更精确地说,它所做的工作是一种统计式的语义生成,而非具备程序语义理解的编译过程。所以,它能否编程,实际上取决于自然语言是否足以描述一个产品级程序。那这可行吗?显然是不行的。在编程领域,自然语言所能起到最大的作用,是描述一个产品的需求,设计的idea。而且即便如此,也要求描述者至少要了解程序的架构,例如C/S,MVC。这些都是架构师的活,

换而言之,我们怎么能幻想让一个核心功能为NLP的系统学会如何构建软件架构?它最多只能把一个初步的设计翻译成一个demo罢了。即使是科幻,阿西莫夫也在他的小说中反复强调,机器人三大定律用自然语言来描述只需要三句话,但在机器人的正子脑中,对应的是数以亿计的量子决策分支,更何况如今的计算机离他所描述的正子脑还远得很。浪漫和信仰都可以理解,但做技术,不能哗众取宠,取悦流量,图个热闹和自嗨。

说真的,无论媒体或者那些网红把LLM吹成什么样子,作为技术人员,我们至少要始终清楚,这类产品聚焦的只是以NLP技术为核心的领域,这只能让机器“听懂”人类的idea,但不等于它能系统性地解决人类的问题。在我个人看来,AI编程能力在可以预见的短期未来,给程序员带来的直接影响大约有三个方面。

  1. 大幅度增加提示词工程师职位,用于产品的最初设计以及demo开发。
  2. 大幅度增加测试岗位,用于产品的质量保证以及快速迭代。
  3. 大幅度增加维护岗位,用于应对更为陡峭的新产品维护曲线。

最后,我想说的是:AI的编程能力不只是对不懂编程的新人不友好(如果只会一些提示词的话),对一堆想依赖ai创业的人会更不友好。因为,AI的编程能力目前最大的作用其实是把一个idea快速变成demo,但是从demo变成真正的产品,恐怕门槛是提高了,而不是降低了,因为由LLM所生成的编程语言编码通常缺乏可维护性、稳定性和架构一致性,导致后期的重构成本上升。如果创业者迷信AI,意识不到这些,他的投资是有很大风险的。

posted on 2025-11-06 11:12  凌杰  阅读(0)  评论(0)    收藏  举报

我发现很多程序员都不会打日志。。。

thbcm阅读(304)

我发现很多程序员都不会打日志。。。

日志不是写给机器看的,是写给未来的你和你的队友看的!

你是小阿巴,刚入职的低级程序员,正在开发一个批量导入数据的程序。

没想到,程序刚上线,产品经理就跑过来说:小阿巴,用户反馈你的程序有 Bug,刚导入没多久就报错中断了!

你赶紧打开服务器,看着比你发量都少的报错信息:

你一脸懵逼:只有这点儿信息,我咋知道哪里出了问题啊?!

你只能硬着头皮让产品经理找用户要数据,然后一条条测试,看看是哪条数据出了问题……

原本大好的摸鱼时光,就这样无了。

这时,你的导师鱼皮走了过来,问道:小阿巴,你是持矢了么?脸色这么难看?

你无奈地说:皮哥,刚才线上出了个 bug,我花了 8 个小时才定位到问题……

鱼皮皱了皱眉:这么久?你没打日志吗?

你很是疑惑:谁是日志?为什么要打它?

鱼皮叹了口气:唉,难怪你要花这么久…… 来,我教你打日志!

⭐️ 本文对应视频版:https://bilibili.com/video/BV1K71yBUEDv

 

什么是日志?

鱼皮打开电脑,给你看了一段代码:

@Slf4j
public class UserService {
   public void batchImport(List<UserDTOuserList) {
       log.info("开始批量导入用户,总数:{}", userList.size());
       
       int successCount 0;
       int failCount 0;
       
       for (UserDTO userDTO : userList) {
           try {
               log.info("正在导入用户:{}", userDTO.getUsername());
               validateUser(userDTO);
               saveUser(userDTO);
               successCount++;
               log.info("用户 {} 导入成功", userDTO.getUsername());
          } catch (Exception e) {
               failCount++;
               log.error("用户 {} 导入失败,原因:{}", userDTO.getUsername(), e.getMessage(), e);
          }
      }
       
       log.info("批量导入完成,成功:{},失败:{}", successCount, failCount);
  }
}

你看着代码里的 log.infolog.error,疑惑地问:这些 log 是干什么的?

鱼皮:这就是打日志。日志用来记录程序运行时的状态和信息,这样当系统出现问题时,我们可以通过日志快速定位问题。

你若有所思:哦?还可以这样!如果当初我的代码里有这些日志,一眼就定位到问题了…… 那我应该怎么打日志?用什么技术呢?

 

怎么打日志?

鱼皮:每种编程语言都有很多日志框架和工具库,比如 Java 可以选用 Log4j 2、Logback 等等。咱们公司用的是 Spring Boot,它默认集成了 Logback 日志框架,你直接用就行,不用再引入额外的库了~

日志框架的使用非常简单,先获取到 Logger 日志对象。

1)方法 1:通过 LoggerFactory 手动获取 Logger 日志对象:

public class MyService {
   private static final Logger logger LoggerFactory.getLogger(MyService.class);
}

2)方法 2:使用 this.getClass 获取当前类的类型,来创建 Logger 对象:

public class MyService {
   private final Logger logger LoggerFactory.getLogger(this.getClass());
}

然后调用 logger.xxx(比如 logger.info)就能输出日志了。

public class MyService {
   private final Logger logger LoggerFactory.getLogger(this.getClass());

   public void doSomething() {
       logger.info("执行了一些操作");
  }
}

效果如图:

 

小阿巴:啊,每个需要打日志的类都要加上这行代码么?

鱼皮:还有更简单的方式,使用 Lombok 工具库提供的 @Slf4j 注解,可以自动为当前类生成日志对象,不用手动定义啦。

@Slf4j
public class MyService {
   public void doSomething() {
       log.info("执行了一些操作");
  }
}

上面的代码等同于 “自动为当前类生成日志对象”:

private static final org.slf4j.Logger log 
   org.slf4j.LoggerFactory.getLogger(MyService.class);

 

你咧嘴一笑:这个好,爽爽爽!

等等,不对,我直接用 Java 自带的 System.out.println 不也能输出信息么?何必多此一举?

System.out.println("开始导入用户" user.getUsername());

 

鱼皮摇了摇头:千万别这么干!

首先,System.out.println 是一个同步方法,每次调用都会导致耗时的 I/O 操作,频繁调用会影响程序的性能。

而且它只能输出信息到控制台,不能灵活控制输出位置、输出格式、输出时机等等。比如你现在想看三天前的日志,System.out.println 的输出早就被刷没了,你还得浪费时间找半天。

 

你恍然大悟:原来如此!那使用日志框架就能解决这些问题吗?

鱼皮点点头:没错,日志框架提供了丰富的打日志方法,还可以通过修改日志配置文件来随心所欲地调教日志,比如把日志同时输出到控制台和文件中、设置日志格式、控制日志级别等等。

在下苦心研究日志多年,沉淀了打日志的 8 大邪修秘法,先传授你 2 招最基础的吧。

 

打日志的 8 大最佳实践

1、合理选择日志级别

第一招,日志分级。

你好奇道:日志还有级别?苹果日志、安卓日志?

鱼皮给了你一巴掌:可不要乱说,日志的级别是按照重要程度进行划分的。

其中 DEBUG、INFO、WARN 和 ERROR 用的最多。

  • 调试用的详细信息用 DEBUG

  • 正常的业务流程用 INFO

  • 可能有问题但不影响主流程的用 WARN

  • 出现异常或错误的用 ERROR

log.debug("用户对象的详细信息:{}", userDTO);  // 调试信息
log.info("用户 {} 开始导入", username);  // 正常流程信息
log.warn("用户 {} 的邮箱格式可疑,但仍然导入", username);  // 警告信息
log.error("用户 {} 导入失败", username, e);  // 错误信息

 

你挠了挠头:俺直接全用 DEBUG 不行么?

鱼皮摇了摇头:如果所有信息都用同一级别,那出了问题时,你怎么快速找到错误信息?

在生产环境,我们通常会把日志级别调高(比如 INFO 或 WARN),这样 DEBUG 级别的日志就不会输出了,防止重要信息被无用日志淹没。

你点点头:俺明白了,不同的场景用不同的级别!

 

2、正确记录日志信息

鱼皮:没错,下面教你第二招。你注意到我刚才写的日志里有一对大括号 {} 吗?

log.info("用户 {} 开始导入", username);

你回忆了一下:对哦,那是啥啊?

鱼皮:这叫参数化日志。{} 是一个占位符,日志框架会在运行时自动把后面的参数值替换进去。

你挠了挠头:我直接用字符串拼接不行吗?

log.info("用户 " username " 开始导入");

鱼皮摇摇头:不推荐。因为字符串拼接是在调用 log 方法之前就执行的,即使这条日志最终不被输出,字符串拼接操作还是会执行,白白浪费性能。

 

你点点头:确实,而且参数化日志比字符串拼接看起来舒服~

 

鱼皮:没错。而且当你要输出异常信息时,也可以使用参数化日志:

try {
   // 业务逻辑
catch (Exception e) {
   log.error("用户 {} 导入失败", username, e);  // 注意这个 e
}

这样日志框架会同时记录上下文信息和完整的异常堆栈信息,便于排查问题。

你抱拳:学会了,我这就去打日志!

 

3、把控时机和内容

很快,你给批量导入程序的代码加上了日志:

@Slf4j
public class UserService {
   public BatchImportResult batchImport(List<UserDTOuserList) {
       log.info("开始批量导入用户,总数:{}", userList.size());
       int successCount 0;
       int failCount 0;
       for (UserDTO userDTO : userList) {
           try {
               log.info("正在导入用户:{}", userDTO.getUsername());   
               // 校验用户名
               if (StringUtils.isBlank(userDTO.getUsername())) {
                   throw new BusinessException("用户名不能为空");
              }
               // 保存用户
               saveUser(userDTO);
               successCount++;
               log.info("用户 {} 导入成功", userDTO.getUsername());
          } catch (Exception e) {
               failCount++;
               log.error("用户 {} 导入失败,原因:{}", userDTO.getUsername(), e.getMessage(), e);
          }
      }
       log.info("批量导入完成,成功:{},失败:{}", successCount, failCount);
       return new BatchImportResult(successCount, failCount);
  }
}

 

光做这点还不够,你还翻出了之前的屎山代码,想给每个文件都打打日志。

 

但打着打着,你就不耐烦了:每段代码都要打日志,好累啊!但是不打日志又怕出问题,怎么办才好?

鱼皮笑道:好问题,这就是我要教你的第三招 —— 把握打日志的时机。

对于重要的业务功能,我建议采用防御性编程,先多多打日志。比如在方法代码的入口和出口记录参数和返回值、在每个关键步骤记录执行状态,而不是等出了问题无法排查的时候才追悔莫及。之后可以再慢慢移除掉不需要的日志。

 

你叹了口气:这我知道,但每个方法都打日志,工作量太大,都影响我摸鱼了!

鱼皮:别担心,你可以利用 AOP 切面编程,自动给每个业务方法的执行前后添加日志,这样就不会错过任何一次调用信息了。

 

你双眼放光:这个好,爽爽爽!

 

鱼皮:不过这样做也有一个缺点,注意不要在日志中记录了敏感信息,比如用户密码。万一你的日志不小心泄露出去,就相当于泄露了大量用户的信息。

你拍拍胸脯:必须的!

 

4、控制日志输出量

一个星期后,产品经理又来找你了:小阿巴,你的批量导入功能又报错啦!而且怎么感觉程序变慢了?

你完全不慌,淡定地打开服务器的日志文件。结果瞬间呆住了……

好家伙,满屏都是密密麻麻的日志,这可怎么看啊?!

鱼皮看了看你的代码,摇了摇头:你现在每导入一条数据都要打一些日志,如果用户导入 10 万条数据,那就是几十万条日志!不仅刷屏,还会影响性能。

你有点委屈:不是你让我多打日志的么?那我应该怎么办?

鱼皮:你需要控制日志的输出量。

1)可以添加条件来控制,比如每处理 100 条数据时才记录一次:

if ((1) 100 == 0) {
   log.info("批量导入进度:{}/{}", 1, userList.size());
}

2)或者在循环中利用 StringBuilder 进行字符串拼接,循环结束后统一输出:

StringBuilder logBuilder new StringBuilder("处理结果:");
for (UserDTO userDTO : userList) {
   processUser(userDTO);
   logBuilder.append(String.format("成功[ID=%s], ", userDTO.getId()));
}
log.info(logBuilder.toString());

3)还可以通过修改日志配置文件,过滤掉特定级别的日志,防止日志刷屏:

<appender name="FILE" class="ch.qos.logback.core.FileAppender">
   <file>logs/app.log</file>
   <!-- 只允许 INFO 级别及以上的日志通过 -->
   <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
       <level>INFO</level>
   </filter>
</appender>

 

5、统一日志格式

你开心了:好耶,这样就不会刷屏了!但是感觉有时候日志很杂很乱,尤其是我想看某一个请求相关的日志时,总是被其他的日志干扰,怎么办?

鱼皮:好问题,可以在日志配置文件中定义统一的日志格式,包含时间戳、线程名称、日志级别、类名、方法名、具体内容等关键信息。

<!-- 控制台日志输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
   <encoder>
       <!-- 日志格式 -->
       <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
   </encoder>
</appender>

这样输出的日志更整齐易读:

 

此外,你还可以通过 MDC(Mapped Diagnostic Context)给日志添加额外的上下文信息,比如请求 ID、用户 ID 等,方便追踪。

在 Java 代码中,可以为 MDC 设置属性值:

@PostMapping("/user/import")
public Result importUsers(@RequestBody UserImportRequest request) {
   // 1. 设置 MDC 上下文信息
   MDC.put("requestId", generateRequestId());
   MDC.put("userId", String.valueOf(request.getUserId()));
   try {
       log.info("用户请求处理完成");      
       // 执行具体业务逻辑
       userService.batchImport(request.getUserList());     
       return Result.success();
  } finally {
       // 2. 及时清理MDC(重要!)
       MDC.clear();
  }
}

然后在日志配置文件中就可以使用这些值了:

<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
   <encoder>
       <!-- 包含 MDC 信息 -->
       <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [%X{requestId}] [%X{userId}] %msg%n</pattern>
   </encoder>
</appender>

这样,每个请求、每个用户的操作一目了然。

 

6、使用异步日志

你又开心了:这样打出来的日志,确实舒服,爽爽爽!但是我打日志越多,是不是程序就会更慢呢?有没有办法能优化一下?

鱼皮:当然有,可以使用 异步日志

正常情况下,你调用 log.info() 打日志时,程序会立刻把日志写入文件,这个过程是同步的,会阻塞当前线程。而异步日志会把写日志的操作放到另一个线程里去做,不会阻塞主线程,性能更好。

你眼睛一亮:这么厉害?怎么开启?

鱼皮:很简单,只需要修改一下配置文件:

<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
   <queueSize>512</queueSize>  <!-- 队列大小 -->
   <discardingThreshold>0</discardingThreshold>  <!-- 丢弃阈值,0 表示不丢弃 -->
   <neverBlock>false</neverBlock>  <!-- 队列满时是否阻塞,false 表示会阻塞 -->
   <appender-ref ref="FILE" />  <!-- 引用实际的日志输出目标 -->
</appender>
<root level="INFO">
   <appender-ref ref="ASYNC" />
</root>

不过异步日志也有缺点,如果程序突然崩溃,缓冲区中还没来得及写入文件的日志可能会丢失。

所以要权衡一下,看你的系统更注重性能还是日志的完整性。

你想了想:我们的程序对性能要求比较高,偶尔丢几条日志问题不大,那我就用异步日志吧。

 

7、日志管理

接下来的很长一段时间,你混的很舒服,有 Bug 都能很快发现。

你甚至觉得 Bug 太少、工作没什么激情,所以没事儿就跟新来的实习生阿坤吹吹牛皮:你知道日志么?我可会打它了!

直到有一天,运维小哥突然跑过来:阿巴阿巴,服务器挂了!你快去看看!

你连忙登录服务器,发现服务器的硬盘爆满了,没法写入新数据。

你查了一下,发现日志文件竟然占了 200GB 的空间!

 

你汗流浃背了,正在考虑怎么甩锅,结果阿坤突然鸡叫起来:阿巴 giegie,你的日志文件是不是从来没清理过?

你尴尬地倒了个立,这样眼泪就不会留下来。

鱼皮叹了口气:这就是我要教你的下一招 —— 日志管理。

你好奇道:怎么管理?我每天登服务器删掉一些历史文件?

鱼皮:人工操作也太麻烦了,我们可以通过修改日志配置文件,让框架帮忙管理日志。

首先设置日志的滚动策略,可以根据文件大小和日期,自动对日志文件进行切分。

<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
   <fileNamePattern>logs/app-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
   <maxFileSize>10MB</maxFileSize>
   <maxHistory>30</maxHistory>
</rollingPolicy>

这样配置后,每天会创建一个新的日志文件(比如 app-2025-10-23.0.log),如果日志文件大小超过 10MB 就再创建一个(比如 app-2025-10-23.1.log),并且只保留最近 30 天的日志。

还可以开启日志压缩功能,进一步节省磁盘空间:

<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
   <!-- .gz 后缀会自动压缩 -->
   <fileNamePattern>logs/app-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
</rollingPolicy>

你有些激动:吼吼,这样我们就可以按照天数更快地查看日志,服务器硬盘也有救啦!

 

8、集成日志收集系统

两年后,你负责的项目已经发展成了一个大型的分布式系统,有好几十个微服务。

如今,每次排查问题你都要登录到不同的服务器上查看日志,非常麻烦。而且有些请求的调用链路很长,你得登录好几台服务器、看好几个服务的日志,才能追踪到一个请求的完整调用过程。

你简直要疯了!

于是你找到鱼皮求助:现在查日志太麻烦了,当年你还有一招没有教我,现在是不是……

鱼皮点点头:嗯,对于分布式系统,就必须要用专业的日志收集系统了,比如很流行的 ELK。

你好奇:ELK 是啥?伊拉克?

阿坤抢答道:我知道,就是 Elasticsearch + Logstash + Kibana 这套组合。

简单来说,Logstash 负责收集各个服务的日志,然后发送给 Elasticsearch 存储和索引,最后通过 Kibana 提供一个可视化的界面。

这样一来,我们可以方便地集中搜索、查看、分析日志。

你惊讶了:原来日志还能这么玩,以后我所有的项目都要用 ELK!

鱼皮摆摆手:不过 ELK 的搭建和运维成本比较高,对于小团队来说可能有点重,还是要按需采用啊。

 

结局

至此,你已经掌握了打日志的核心秘法。

只是你很疑惑,为何那阿坤竟对日志系统如此熟悉?

阿坤苦笑道:我本来就是日志管理大师,可惜我上家公司的同事从来不打日志,所以我把他们暴打了一顿后跑路了。

阿巴 giegie 你要记住,日志不是写给机器看的,是写给未来的你和你的队友看的!

你要是以后不打日志,我就打你!

 

更多编程学习资源

posted @
2025-11-06 11:29 
程序员鱼皮  阅读(
0)  评论(
0)   
收藏 
举报

AD 横向移动-LSASS 进程转储

thbcm阅读(503)

首先,我们在 RDP 的桌面环境下使用任务管理器去转储 LSASS 进程;然后,在无 GUI 的命令行环境下使用 7+2 种方式去转储 LSASS 进程。

联系我们