让运维无忧,实战解析巡检报告功能实现方案

news2024/11/23 18:52:08

随着大数据技术的演进和信息安全性需求的提升,数据规模的持续扩张为数据运维工作带来了严峻考验。面对海量数据所形成的繁重管理压力,运维人员面临效率瓶颈,而不断攀升的人力成本也使得单纯依赖扩充运维团队来解决问题变得不再实际可行。

由此可见,智能化与高效便捷是运维发展的必然方向。袋鼠云所推出的巡检报告功能,正是为了顺应这一目标,致力于提供优化的解决方案。

什么是巡检报告?

巡检报告是指对某一个系统或设备进行全面检查,并把检查结果及建议整理成报告的过程。巡检报告通常用于评估系统或设备的运行状况与性能,为发现问题、优化系统、提高效率、降低故障率等方面提供参考。

file

本文将详细阐述巡检报告的各项功能特性和其实现方案,为有此类需求的用户提供实用的参考依据。

巡检报告实现功能

● 自定义布局

· 报告中的面板可进行拖拽改变布局

· 在拖拽的过程中限制拖拽区域,只允许在同一父级内进行拖拽,不允许跨目录移动,不允许改变目录的级别,比如把一级目录移动到另一个一级目录内,变成二级目录

● 目录可收缩展开

· 目录支持收缩展开,收缩时隐藏所有子面板,展开时显示所有子面板

· 移动目录时,子面板跟随移动

· 改变目录后,同步更新右侧的目录面板

· 生成目录编号

file

● 右侧目录树

· 生成目录编号

· 支持锚点滚动

· 支持展开收缩

· 与左侧报告联动

file

● 数据面板

· 根据日期范围获取指标数据

· 通过图表的形式展示指标信息

· 查看详情,删除

· 各面板的请求设计,支持刷新请求

file

file

● 面板导入

· 统计目录下选择的面板数量

· 导入新面板时,不能破坏已有布局,新面板只能跟在旧面板后

· 导入已有面板时,需要进行数据比较,有数据变更需要重新获取最新的数据

file

● 保存

在保存前,所有影响布局相关的操作,都是临时的,包括导入面板。只有在点击保存后,才会把当前数据提交给后端进行保存。

● 支持 pdf 和 word 导出

file

巡检报告实现方案

那么,这一套巡检报告功能究竟是如何实现的呢?下面将从数据结构设计、组件设计、目录、面板等方面进行逐一介绍。

数据结构设计

先看看使用扁平结构下的图示:

file

在扁平结构下,确定子项只需要找到下一个 row 面板,对于多级目录下也是同理,只是对一级目录需要额外处理。

扁平结构虽然实现起来较为简单,但为了满足特定需求,即限制目录的拖拽。限制目录需要一个比较清晰的面板层级关系,很显然,树状数据结构能够非常贴切且清晰地描述一个数据的层级结构。

file

组件设计

与传统组件编程有所区别。在实现上对渲染和数据处理进行了分离,分为两块:

· React 组件:主要负责页面渲染

· Class : 负责数据的处理

file

DashboardModel

class DashboardModel {
    id: string | number;
    panels: PanelModel[]; // 各个面板
    // ...
}

PanelModel

class PanelModel {
    key?: string;
    id!: number;
    gridPos!: GridPos; // 位置信息
    title?: string;
    type: string;
    panels: PanelModel[]; // 目录面板需要维护当前目录下的面板信息
    // ...
}

每一个 Dashboard 组件对应一个 DashboardModel,每一个 Panel 组件对应一个 PanelModel。

React 组建根据类实例中的数据进行渲染。实例生产后,不会轻易的销毁,或者改变引用地址,这让依赖实例数据进行渲染的 React 组件无法触发更新渲染。

需要一个方式,在实例内数据发生改变后,由我们手动触发组件的更新渲染。

● 组件渲染控制

由于我们之前采用的是 Hooks 组件,不像 Class 组件可以通过调用 forceUpdate 方法触发组件。

而在 react18 中有一个新特性 useSyncExternalStore,可以让我们订阅外部的数据,如果数据发生改变了,会触发组件的渲染。

实际上 useSyncExternalStore 触发组件渲染的原理就是在内部维护了一个 state,当更改了 state 值,则引起了外部组件的渲染。

基于这个思路我们简单的实现了一个能够触发组件渲染的 useForceUpdate 方法。

export function useForceUpdate() {
    const [_, setValue] = useState(0);
    return debounce(() => setValue((prevState) => prevState + 1), 0);
}

虽说实现了 useForceUpdate,但是在实际使用的过程中,还需要在组件销毁时移除事件。而 useSyncExternalStore 已经内部已经实现了,直接使用即可。

useSyncExternalStore(dashboard?.subscribe ?? (() => {}), dashboard?.getSnapshot ?? (() => 0));

useSyncExternalStore(panel?.subscribe ?? (() => {}), panel?.getSnapshot ?? (() => 0));

根据 useSyncExternalStore 使用,分别添加了 subscribe 和 getSnapshot 方法。

class DashboardModel {  // PanelModel 一样 
    count = 0;

    forceUpdate() {
        this.count += 1;
        eventEmitter.emit(this.key);
    }

    /**
     * useSyncExternalStore 的第一个入参,执行 listener 可以触发组件的重渲染
     * @param listener
     * @returns
     */
    subscribe = (listener: () => void) => {
        eventEmitter.on(this.key, listener);
        return () => {
            eventEmitter.off(this.key, listener);
        };
    };

    /**
     * useSyncExternalStore 的第二个入参,count 在这里改变后触发diff的通过。
     * @param listener
     * @returns
     */
    getSnapshot = () => {
        return this.count;
    };
}

当改变数据后,需要触发组件的渲染,只需要执行 forceUpdate 即可。

面板

● 面板拖拽

市面上比较大众的拖拽插件有以下几个:

· react-beautiful-dnd

· react-dnd

· react-grid-layout

经过比较后,发现 react-grid-layout 非常适合用来做面板的拖拽功能。react-grid-layout 本身使用简单,基本无上手门槛,最终决定使用 react-grid-layout。详细说明可以查看该链接:https://github.com/react-grid-layout/react-grid-layout

在面板布局改变后触发 react-grid-layout 的 onLayoutChange 方法,可以拿到布局后的所有面板最新的位置数据。

const onLayoutChange = (newLayout: ReactGridLayout.Layout[]) => {
    for (const newPos of newLayout) {
        panelMap[newPos.i!].updateGridPos(newPos);
    }
    dashboard!.sortPanelsByGridPos();
};

PanelMap 是一个 map,key 为 Panel.key, value 为面板,是在我们组件渲染时就已经准备好了。

const panelMap: Record<PanelModel['key'], PanelModel> = {};

要更新面板布局数据,可通过 PanelMap 准确定位到相应的面板,并进一步调用其 updateGridPos 方法执行布局更新操作。

到这,我们只是完成了面板本身数据更新,还需要执行仪表盘的 sortPanelsByGridPos 方法,对所有的面板进行排序。

class DashboardModel {
    sortPanelsByGridPos() {
        this.panels.sort((panelA, panelB) => {
            if (panelA.gridPos.y === panelB.gridPos.y) {
                return panelA.gridPos.x - panelB.gridPos.x;
            } else {
                return panelA.gridPos.y - panelB.gridPos.y;
            }
        });
    }
    // ...
}

● 面板拖动范围

目前的拖动范围是整个仪表盘,可随意拖动,绿色是仪表盘可拖拽区域,灰色为面板。如下:

file

如果需要限制就需要改成如下图的结构:

file

在原本的基础上,以目录为单位区分,绿色为整体的可移动区域,黄色为一级目录块,可在绿色区域拖动,拖动时以整个黄色块进行拖动,紫色为二级目录块,可在当前黄色区域内拖动,不可脱离当前黄色块,灰色的面板只能在当前目录下拖动。

需要在原先数据结构基础上进行改造:

file

class PanelModel {
    dashboard?: DashboardModel; // 当前目录下的 dashboard
    // ...
}

● 面板的导入设计

file

后端返回的数据是一颗有着三级层级的树,我们拿到后,在数据上维护成 ModuleMap, DashboardMap 和 PanelMap 3个Map。

import { createContext } from 'react';

export interface Module { // 一级目录
    key: string;
    label: string;
    dashboards?: string[];
    sub_module?: Dashboard[];
}

export interface Dashboard { // 二级目录
    key: string;
    dashboard_key: string;
    label: string;
    panels?: number[];
    selectPanels?: number[];
    metrics?: Panel[];
}

export interface Panel {
    expr: Expr[]; // 数据源语句信息
    label: string;
    panel_id: number;
}

type Expr = {
    expr: string;
    legendFormat: string;
};

export const DashboardContext = createContext({
    moduleMap: new Map<string, Module>(),
    dashboardMap: new Map<string, Dashboard>(),
    panelMap: new Map<number, Panel>(),
});

我们在渲染模块时,遍历 ModuleMap ,并通过 Module 内的 dashboards 信息找到二级目录。

在交互上设置一级目录不可选中,当选中二级目录时,通过二级目录 Dashboard 的 panels 找到相关的面板渲染到右侧区域。

对于这3个 Map 的操作,维护在 useHandleData 中,导出:

{
    ...map, // moduleMap、dashboardMap、panelMap
    getData, // 生成巡检报告的数据结构
    init: initData, // 初始化 Map
}

● 面板选中回填

在进入面板管理时,需要回填已选中的面板,我们可以通过 getSaveModel 获取到当前巡检报告的信息,把对应的选中信息存放到 selectPanels 中。

现在我们只需要改变 selectPanels 中的值,就可以做到对应面板的选中。

● 面板选中重置

直接遍历 DashboardMap,并把每个 selectPanels 重置。

dashboardMap.forEach((dashboard) => {
    dashboard.selectPanels = [];
});

● 面板插入

在我们选中面板后,对选中面板进行插入时,有几种情况:

· 巡检报告原本存在的面板,这次也选中,在插入时会比较数据,如果数据发生改变,需要根据最新的数据源信息进行请求,并渲染

· 巡检报告原本存在的面板,这次未选中,在插入时,需要删除掉未选中的面板

· 新选中的面板,在插入时,在对应目录的末尾进行插入

添加新面板需要,与目录收缩类似,不同的是:

· 目录收缩针对的只有一个目录,而插入在针对的是整体

· 目录收缩是直接从子节点开始向上冒泡,而插入是先从根节点开始向下插入,插入完成后在根据最新的目录数据,更新一遍布局

class DashboardModel {
    update(panels: PanelData[]) {
        this.updatePanels(panels); // 更新面板
        this.resetDashboardGridPos(); // 重新布局
        this.forceUpdate();
    }

    /**
     * 以当前与传入的进行对比,以传入的数据为准,并在当前的顺序上进行修改
     * @param panels
     */
    updatePanels(panels: PanelData[]) {
        const panelMap = new Map();
        panels.forEach((panel) => panelMap.set(panel.id, panel));

        this.panels = this.panels.filter((panel) => {
            if (panelMap.has(panel.id)) {
                panel.update(panelMap.get(panel.id));
                panelMap.delete(panel.id);
                return true;
            }
            return false;
        });

        panelMap.forEach((panel) => {
            this.addPanel(panel);
        });
    }

    addPanel(panelData: any) {
        this.panels = [...this.panels, new PanelModel({ ...panelData, top: this })];
    }

    resetDashboardGridPos(panels: PanelModel[] = this.panels) {
        let sumH = 0;
        panels?.forEach((panel: any | PanelModel) => {
            let h = ROW_HEIGHT;
            if (isRowPanel(panel)) {
                h += this.resetDashboardGridPos(panel.dashboard.panels);
            } else {
                h = panel.getHeight();
            }

            const gridPos = {
                ...panel.gridPos,
                y: sumH,
                h,
            };
            panel.updateGridPos({ ...gridPos });
            sumH += h;
        });

        return sumH;
    }
}

class PanelModel {
    /**
     * 更新
     * @param panel
     */
    update(panel: PanelData) {
        // 数据源语句发生变化需要重新获取数据
        if (this.target !== panel.target) {
            this.needRequest = true;
        }

        this.restoreModel(panel);

        if (this.dashboard) {
            this.dashboard.updatePanels(panel.panels ?? []);
        }

        this.needRequest && this.forceUpdate();
    }
}

● 面板请求

needRequest 控制面板是否需要进行请求,如果为 true 在面板下一次进行渲染时,会进行请求,请求的处理也放在了 PanelModel 中。

import { Params, params as fetchParams } from '../../components/useParams';

class PanelModel {
    target: string; // 数据源信息

    getParams() {
        return {
            targets: this.target,
            ...fetchParams,
        } as Params;
    }

    request = () => {
        if (!this.needRequest) return;
        this.fetchData(this.getParams());
    };

    fetchData = async (params: Params) => {
        const data = await this.fetch(params);
        this.data = data;
        this.needRequest = false;
        this.forceUpdate();
    };

    fetch = async (params: Params) => { /* ... */ }
}

我们数据渲染组件一般层级较深,而请求时会需要时间区间等外部参数,对于这部分参数采用全局变量的方式,用 useParams 进行维护。上层组件使用 change 修改参数,数据渲染组件根据抛出的 params 进行请求。

export let params: Params = {
    decimal: 1,
    unit: null,
};

function useParams() {
    const change = (next: (() => Params) | Params) => {
        if (typeof next === 'function') params = next();
        params = { ...params, ...next } as Params;
    };

    return { params, change };
}

export default useParams;

● 面板刷新

从根节点向下查找,找到叶子节点,在触发对应的请求。

file

class DashboardModel {
    /**
     * 刷新子面板
     */
    reloadPanels() {
        this.panels.forEach((panel) => {
            panel.reload();
        });
    }
}

class PanelModel {
    /**
     * 刷新
     */
    reload() {
        if (isRowPanel(this)) {
            this.dashboard.reloadPanels();
        } else {
            this.reRequest();
        }
    }

    reRequest() {
        this.needRequest = true;
        this.request();
    }
}

● 面板的删除

对于面板的删除,我们只需要在对应的 Dashboard 下进行移除,删除后会改变当前 Dashboard 高度,这块的处理与下文的目录收缩一致。

class DashboardModel {
    /**
     * @param panel 删除的面板
     */
    removePanel(panel: PanelModel) {
        this.panels = this.filterPanelsByPanels([panel]);

        // 冒泡父容器,减少的高度
        const h = -panel.gridPos.h;
        this.top?.changeHeight(h);

        this.forceUpdate();
    }

    /**
     * 根据传入的面板进行过滤
     * @param panels 需要过滤的面板数组
     * @returns 过滤后的面板
     */
    filterPanelsByPanels(panels: PanelModel[]) {
        return this.panels.filter((panel) => !panels.includes(panel));
    }
    // ...
}

● 面板的保存

与后端沟通后,当前巡检报告数据结构由前端自主维护,最终给后端一个字符串就好。获取到目前的面板数据,用 JSON 进行转换即可。

面板的信息获取过程,先从根节点出发,遍历至叶子结点,再从叶子结点开始,一层层向上进行返回,也就是回溯的过程。

class DashboardModel {
    /**
     * 获取所有面板数据
     * @returns
     */
    getSaveModel() {
        const panels: PanelData[] = this.panels.map((panel) => panel.getSaveModel());
        return panels;
    }
    // ...
}

// 最终保存时所需要的属性,其他的都不需要
const persistedProperties: { [str: string]: boolean } = {
    id: true,
    title: true,
    type: true,
    gridPos: true,
    collapsed: true,
    target: true,
};

class PanelModel {
    /**
     * 获取所有面板数据
     * @returns
     */
    getSaveModel() {
        const model: any = {};

        for (const property in this) {
            if (persistedProperties[property] && this.hasOwnProperty(property)) {
                model[property] = cloneDeep(this[property]);
            }
        }
        model.panels = this.dashboard?.getSaveModel() ?? [];

        return model;
    }
    // ...
}

● 面板详情展示

file

对面板进行查看时,可修改时间等,这些操作会影响到实例中的数据,需要对原数据与详情中的数据进行区分。

通过对原面板数据的重新生成一个 PanelModel 实例,对这个实例进行任意操作,都不会影响到原数据。

const model = panel.getSaveModel();
const newPanel = new PanelModel({ ...model, top: panel.top }); // 创建一个新的实例
setEditPanel(newPanel); // 设置为详情

在 dom 上,详情页面是采用绝对定位,覆盖着巡检报告。

目录

● 目录收缩展开

为目录面板维护一个 collapsed 属性用来控制面板的隐藏显示。

class PanelModel {
    collapsed?: boolean; // type = row
    // ...
}

// 组件渲染
{!collapsed && <DashBoard dashboard={panel.dashboard} serialNumber={serialNumber} />}

对目录进行收缩展开时,会改变自身的高度,现在还需要把这个改变的高度同步给上一级的仪表盘。

上一级需要做的就是类似我们控制目录的处理。如下,控制第一个二级目录收缩:

file

当面板发生变更时,需要通知上级面板,进行对应的操作。

file

增加一个 top 用来获取到父级实例。

class DashboardModel {
    top?: null | PanelModel; // 最近的 panel 面板

    /**
     * 面板高度变更,同步修改其他面板进行对应高度 Y 轴的变更
     * @param row 变更高度的 row 面板
     * @param h 变更高度
     */
    togglePanelHeight(row: PanelModel, h: number) {
        const rowIndex = this.getIndexById(row.id);

        for (let panelIndex = rowIndex + 1; panelIndex < this.panels.length; panelIndex++) {
            this.panels[panelIndex].gridPos.y += h;
        }
        this.panels = [...this.panels];

        // 顶级 dashBoard 容器没有 top
        this.top?.changeHeight(h);
        this.forceUpdate();
    }
    // ...
}

class PanelModel {
    top: DashboardModel; // 最近的 dashboard 面板

    /**
     * @returns h 展开收起影响的高度
     */
    toggleRow() {
        this.collapsed = !this.collapsed;
        let h = this.dashboard?.getHeight();
        h = this.collapsed ? -h : h;
        this.changeHeight(h);
    }

    /**
     *
     * @param h 变更的高度
     */
    changeHeight(h: number) {
        this.updateGridPos({ ...this.gridPos, h: this.gridPos.h + h }); // 更改自身面板的高度
        this.top.togglePanelHeight(this, h); // 触发父级变更
        this.forceUpdate();
    }
    // ...
}

整理流程与冒泡类型,一直到最顶级的 Dashboard。展开收缩同理。

file

● 右侧目录渲染

锚点/序号

· 锚点采用 Anchor + id 选中组件

· 序号根据每次渲染进行生成

采用发布订阅管理渲染

每当仪表盘改变布局的动作时,右侧目录就需要进行同步更新,而任意一个面板都有可能需要触发右侧目录的更新。

如果我们采用实例内维护对应组件的渲染事件,有两个问题:

· 需要进行区分,比如刷新面板时,不需要触发右侧目录的渲染

· 每个面板如何订阅右侧目录的渲染事件

最终采用了发布订阅者模式,对事件进行管理。

class EventEmitter {
    list: Record<string, any[]> = {};

    /**
     * 订阅
     * @param event 订阅事件
     * @param fn 订阅事件回调
     * @returns
     */
    on(event: string, fn: () => void) {}

    /**
     * 取消订阅
     * @param event 订阅事件
     * @param fn 订阅事件回调
     * @returns
     */
    off(event: string, fn: () => void) {}

    /**
     * 发布
     * @param event 订阅事件
     * @param arg 额外参数
     * @returns
     */
    emit(event: string, ...arg: any[]) {
}
eventEmitter.emit(this.key); // 触发面板的订阅事件

eventEmitter.emit(GLOBAL); // 触发顶级订阅事件,就包括右侧目录的更新

pdf/word 导出

pdf 导出由 html2Canvas + jsPDF 实现。需要注意的是,当图片过长 pdf 会对图片进行切分,有可能出现切分的是内容区域的情况。需要手动计算面板的高度,是否超出当前文档,如果超出需要我们提前进行分割,添加到下一页中,尽可能把目录面板和数据面板一块切分。

word 导出由 html-docx-js 实现, 需要保留目录的结构,并可以在面板下添加总结,这就需要我们分别对每一个面板进行图片的转换。

实现的思路是根据 panels 遍历,找到目录面板就是用 h1、h2 标签插入,如果是数据面板,在数据面板中维护一个 ref 的属性,能让我们拿到当前面板的 dom 信息,根据这个进行图片转换,并为 base64 的格式(word 只支持 base64 的图片插入)。

写在最后

当前版本的巡检报告尚处于初级阶段,并非最终形态,随着后续的迭代升级,我们将逐步添加包括总结说明在内的多项功能。

采用目前方式实现后,未来若需进行 UI 界面调整时,只需针对性地修改相关 UI 组件即可,例如新增饼图、表格等内容。而在数据交互层面的改动,则仅需进入 DashboardModel 和 PanelModel 中进行必要的更新。此外,针对特定场景,我们还可以灵活抽离出专用类来进行处理,以确保整个迭代过程更加模块化和高效化。

《数栈产品白皮书》下载地址:https://www.dtstack.com/resources/1004?src=szsm

《数据治理行业实践白皮书》下载地址:https://www.dtstack.com/resources/1001?src=szsm

想了解或咨询更多有关大数据产品、行业解决方案、客户案例的朋友,浏览袋鼠云官网:https://www.dtstack.com/?src=szcsdn

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1493912.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

双体系Java学习之全路线图

Java路线图 此路线图是为了我以后的Java学习指明方向。 希望大家都能在Java的路线上越走越远&#xff01;努力学习&#xff01;&#xff01;

自动驾驶革命:解密端到端背后的数据、算力和AI奇迹

作者 |毫末智行数据智能科学家 贺翔 编辑 |祥威 最近&#xff0c;特斯拉FSD V12的发布引发了业界对端到端自动驾驶的热议&#xff0c;业界纷纷猜测FSD V12的强大能力是如何训练出来的。从马斯克的测试视频可以大致归纳一下FSD V12系统的一些核心特征&#xff1a; 训练数据&am…

FreeRTOS操作系统学习——空闲任务及其钩子函数

空闲任务 当 FreeRTOS 的调度器启动以后就会自动的创建一个空闲任务&#xff0c;这样就可以确保至少有一任务可以运行。但是这个空闲任务使用最低优先级&#xff0c;如果应用中有其他高优先级任务处于就绪态的话这个空闲任务就不会跟高优先级的任务抢占 CPU 资源。空闲任务还有…

图机器学习(1)--导论

0 CS224W概况 斯坦福大学CS224W课程&#xff1a;http://cs224w.stanford.edu/ 图机器学习的库&#xff1a; 为什么是图&#xff1f;图是描述和分析具有关系/交互的实体的通用语言。 1 图数据举例 复杂域具有丰富的关系结构&#xff0c;可以表示为关系图。 通过显式地建模关…

比瓴科技强势领跑软件开发安全领域,ASPM名列赛道第一

近日&#xff0c;斯元商业咨询正式发布2024首版「网安新兴赛道厂商速查指南&#xff5c;短名单精选」。比瓴科技入围七个赛道&#xff0c;其中ASPM、ASOC、SDL位居赛道第一。 应用安全态势管理&#xff08;ASPM&#xff09; 降低应用安全漏洞及数据泄露风险 比瓴在软件安全领…

seliunx 基础规则介绍

一 SELinux的状态 enforcing&#xff1a;强制&#xff0c;每个受限的进程都必然受限 permissive&#xff1a;允许&#xff0c;每个受限的进程违规操作不会被禁止&#xff0c;但会被记录于审计日志 disabled&#xff1a;禁用 二 相关命令 getenforce: 获取selinux当前状…

SDWAN专线,解决银行网络搭建痛点

金融行业的不断发展和数字化转型&#xff0c;银行网络的搭建和管理面临着诸多挑战和痛点。SD-WAN&#xff08;Software-Defined Wide Area Network&#xff0c;软件定义广域网&#xff09;专线作为一种创新的网络解决方案&#xff0c;为银行解决了诸多网络搭建痛点&#xff0c;…

Hadoop集群配置与管理指南

目录 前言一、Hadoop集群配置历史服务器二、配置日志的聚集三、集群启动/停止方式总结四、编写Hadoop集群常用脚本五、常用端口号说明最后 前言 这篇文章内容覆盖了Hadoop集群中一些重要且常用的配置和管理任务。首先&#xff0c;我们将介绍如何配置Hadoop集群的历史服务器&am…

基于ceph-deploy部署Ceph 集群

Ceph分布式存储一、存储基础1、单机存储设备1.1 单机存储的问题 2、分布式存储(软件定义的存储SDS)2.1 分布式存储的类型 二、Ceph简介1、Ceph优势2、Ceph架构3、Ceph 核心组件4、OSD 存储后端5、Ceph 数据的存储过程6、Ceph 版本发行生命周期 三、Ceph 集群部署1、 基于 ceph-…

java: No enum constant javax.lang.model.element.Modifier.SEALED报错

这里我的idea版本为2021.03&#xff0c;JDK版本为21.0.2。经过大量冲浪后大多数都是让修改JDK版本&#xff0c;原因是Modifier.SEALED是JDK15新增的&#xff0c;但是当我修改完JDK版本后并无卵用。 尝试在代码中声明&#xff0c;也没问题可以引用到&#xff0c;这就怪了&#…

AI付费课程水分大 网红博主李一舟卖课被下架

日前&#xff0c;OpenAI旗下的文生视频模型Sora爆火&#xff0c;网上的AI付费课程嗅到商机&#xff0c;开始上线大量相关教学视频&#xff0c;几元至百元就号称能从入门小白到大神&#xff0c;其中就包括自称清华博士的李一舟。不过&#xff0c;李一舟很快就翻车了&#xff0c;…

6个免费可商用的高清图片素材网站,建议收藏!

作为设计师或者是自媒体创作者&#xff0c;都需要寻找高质量的图片素材为作品增添色彩&#xff0c;但随意找的图片素材很容易侵权。为了让大家能找到免费又能商用的图片素材&#xff0c;这期分享我经常用的6个图片素材网站&#xff0c;免费下载还能商用&#xff0c;赶紧收藏起来…

【产品经理方法论——产品的基本概念】

1. 产品学三元素 产品学有三个元素&#xff1a;用户、需求、产品 产品学的内容&#xff1a;根据用户的需求设计产品&#xff0c;使用产品服务用户 仅仅通过三个元素无法说明每个元素的概念&#xff0c;因为三个元素互为说明关系。 通过引入人/群体来说明三个元素的关系。 需…

腾讯云最新活动_腾讯云促销优惠_代金券-腾讯云官网入口

腾讯云服务器多少钱一年&#xff1f;62元一年起&#xff0c;2核2G3M配置&#xff0c;腾讯云2核4G5M轻量应用服务器218元一年、756元3年&#xff0c;4核16G12M服务器32元1个月、312元一年&#xff0c;8核32G22M服务器115元1个月、345元3个月&#xff0c;腾讯云服务器网txyfwq.co…

20240305-2-海量数据处理常用技术概述

海量数据处理常用技术概述 如今互联网产生的数据量已经达到PB级别&#xff0c;如何在数据量不断增大的情况下&#xff0c;依然保证快速的检索或者更新数据&#xff0c;是我们面临的问题。 所谓海量数据处理&#xff0c;是指基于海量数据的存储、处理和操作等。因为数据量太大无…

985硕的4家大厂实习与校招经历专题分享(part2)

我的个人经历&#xff1a; 985硕士24届毕业生&#xff0c;实验室方向:CV深度学习 就业&#xff1a;工程-java后端 关注大模型相关技术发展 校招offer: 阿里巴巴 字节跳动 等10 研究生期间独立发了一篇二区SCI 实习经历:字节 阿里 京东 B站 &#xff08;只看大厂&#xff0c;面试…

抖店应该怎么运营,2024新版入门教程分享,快速起店玩法

我是王路飞。 抖店快速起店的方法有很多&#xff0c;爆款截流、低价引流、全店动销、货损起店、达人带货等等。 今天主要给你们说下达人带货爆款截流的玩法&#xff0c;你们相结合着去做。 感兴趣的可以先收藏并关注&#xff0c;文末也有免费的抖店资料和领取。 内容来源于…

容器化技术

容器化技术并不是由Docker引入&#xff0c;而是有其发展历程。容器有效地将由单个操作系统管理的资源划分到孤立的组中&#xff0c;以更好地在孤立的组之间平衡有冲突的资惊使用需求。容器可以在核心CPU运行指令&#xff0c;而不需要任何专门的解释机制。容器避免了准虚拟化(pa…

Latte:一个类似Sora的开源视频生成项目

前段时间OpenAI发布的Sora引起了巨大的轰动&#xff0c;最长可达1分钟的高清连贯视频生成能力秒杀了一众视频生成玩家。因为Sora没有公开发布&#xff0c;网上对Sora的解读翻来覆去就那么多&#xff0c;我也不想像复读机一样再重复一遍了。 本文给大家介绍一个类似Sora的视频生…

第五篇:组件更新:完整的 DOM diff 流程是怎样的?(下)

下面我们来继续讲解上节课提到的核心 diff 算法。 新子节点数组相对于旧子节点数组的变化&#xff0c;无非是通过更新、删除、添加和移动节点来完成&#xff0c;而核心 diff 算法&#xff0c;就是在已知旧子节点的 DOM 结构、vnode 和新子节点的 vnode 情况下&#xff0c;以较…