ZMQ之脱机可靠性--巨人模式

news2024/11/24 17:28:25

        当你意识到管家模式是一种非常可靠的消息代理时,你可能会想要使用磁盘做一下消息中转,从而进一步提升可靠性。这种方式虽然在很多企业级消息系统中应用,但我还是有些反对的,原因有:

                1、我们可以看到,懒惰海盗模式的client可以工作得非常好,能够在多种架构中运行。唯一的问题是它会假设worker是无状态的,且提供的服务是幂等的。但这个问题我们可以通过其他方式解决,而不是添加磁盘。

                2、添加磁盘会带来新的问题,需要额外的管理和维护费用。海盗模式的最大优点就是简单明了,不会崩溃。如果你还是担心硬件会出问题,可以改用点对点的通信模式,这会在本章最后一节讲到。

        虽然有以上原因,但还是有一个合理的场景可以用到磁盘中转的——异步脱机网络。海盗模式有一个问题,那就是client发送请求后会一直等待应答。如果client和worker并不是长连接(可以拿电子邮箱做个类比),我们就无法在client和worker之间建立一个无状态的网络,因此需要将这种状态保存起来。

        于是我们就有了巨人模式,该模式下会将消息写到磁盘中,确保不会丢失。当我们进行服务查询时,会转向巨人这一层进行。巨人是建立在管家之上的,而不是改写了MDP协议。这样做的好处是我们可以在一个特定的worker中实现这种可靠性,而不用去增加代理的逻辑。

        实现更为简单:

                1、代理用一种语言编写,worker使用另一种语言编写。

                2、可以自由升级这种模式。

        唯一的缺点是,代理和磁盘之间会有一层额外的联系,不过这也是值得的。

        我们有很多方法来实现一种持久化的请求-应答架构,而目标当然是越简单越好。我能想到的最简单的方式是提供一种成为“巨人”的代理服务,它不会影响现有worker的工作,若client想要立即得到应答,它可以和代理进行通信;如果它不是那么着急,那就可以和巨人通信:“嗨,巨人,麻烦帮我处理下这个请求,我去买些菜。”

         这样一来,巨人就既是worker又是client。client和巨人之间的对话一般是:

                1、Client: 请帮我处理这个请求。巨人:好的。

                2、Client: 有要给我的应答吗?巨人:有的。(或者没有)

                3、Client: OK,你可以释放那个请求了,工作已经完成。巨人:好的。

        巨人和代理之间的对话一般是:

                1、巨人:嗨,代理程序,你这里有个叫echo的服务吗?代理:恩,好像有。

                2、巨人:嗨,echo服务,请帮我处理一下这个请求。Echo: 好了,这是应答。

                3、巨人:谢谢!

        你可以想象一些发生故障的情形,看看上述模式是否能解决?worker在处理请求的时候崩溃,巨人会不断地重新发送请求;应答在传输过程中丢失了,巨人也会重试;如果请求已经处理,但client没有得到应答,那它会再次询问巨人;如果巨人在处理请求或进行应答的时候崩溃了,客户端会进行重试;只要请求是被保存在磁盘上的,那它就不会丢失。

        这个机制中,握手的过程是比较漫长的,但client可以使用异步的管家模式,一次发送多个请求,并一起等待应答。

        我们需要一种方法,让client会去请求应答内容。不同的client会访问到相同的服务,且client是来去自由的,有着不同的标识。一个简单、合理、安全的解决方案是:

                1、当巨人收到请求时,它会为每个请求生成唯一的编号(UUID),并将这个编号返回给client;

                2、client在请求应答内容时需要提供这个编号。

        这样一来client就需要负责将UUID安全地保存起来,不过这就省去了验证的过程。有其他方案吗?我们可以使用持久化的套接字,即显式声明客户端的套接字标识。然而,这会造成管理上的麻烦,而且万一两个client的套接字标识相同,那会引来无穷的麻烦。

        在我们开始制定一个新的协议之前,我们先思考一下client如何和巨人通信。一种方案是提供一种服务,配合三个不同的命令;另一种方案则更为简单,提供三种独立的服务:

                1、titanic.request - 保存一个请求,并返回UUID

                2、titanic.reply - 根据UUID获取应答内容

                3、titanic.close - 确认某个请求已被正确地处理

        我们需要创建一个多线程的worker,正如我们之前用ZMQ进行多线程编程一样,很简单。但是,在我们开始编写代码之前,先讲巨人模式的一些定义写下来:http://rfc.zeromq.org/spec:9 。我们称之为“巨人服务协议”,或TSP。

        使用TSP协议自然会让client多出额外的工作,下面是一个简单但足够健壮的client:

        ticlient: Titanic client example in C

//
//  巨人模式client示例
//  实现 http://rfc.zeromq.org/spec:9 协议中的client端
 
//  让我们直接编译,不创建类库
#include "mdcliapi.c"
 
//  请求TSP协议下的服务
//  如果成功则返回应答(状态码:200),否则返回NULL
//
static zmsg_t *
s_service_call (mdcli_t *session, char *service, zmsg_t **request_p)
{
    zmsg_t *reply = mdcli_send (session, service, request_p);
    if (reply) {
        zframe_t *status = zmsg_pop (reply);
        if (zframe_streq (status, "200")) {
            zframe_destroy (&status);
            return reply;
        }
        else
        if (zframe_streq (status, "400")) {
            printf ("E: 客户端发生严重错误,取消请求\n");
            exit (EXIT_FAILURE);
        }
        else
        if (zframe_streq (status, "500")) {
            printf ("E: 服务端发生严重错误,取消请求\n");
            exit (EXIT_FAILURE);
        }
    }
    else
        exit (EXIT_SUCCESS);    //  中断或发生错误
 
    zmsg_destroy (&reply);
    return NULL;        //  请求不成功,但不返回失败原因
}
 
int main (int argc, char *argv [])
{
    int verbose = (argc > 1 && streq (argv [1], "-v"));
    mdcli_t *session = mdcli_new ("tcp://localhost:5555", verbose);
 
    //  1. 发送echo服务的请求给巨人
    zmsg_t *request = zmsg_new ();
    zmsg_addstr (request, "echo");
    zmsg_addstr (request, "Hello world");
    zmsg_t *reply = s_service_call (
        session, "titanic.request", &request);
 
    zframe_t *uuid = NULL;
    if (reply) {
        uuid = zmsg_pop (reply);
        zmsg_destroy (&reply);
        zframe_print (uuid, "I: request UUID ");
    }
 
    //  2. 等待应答
    while (!zctx_interrupted) {
        zclock_sleep (100);
        request = zmsg_new ();
        zmsg_add (request, zframe_dup (uuid));
        zmsg_t *reply = s_service_call (
            session, "titanic.reply", &request);
 
        if (reply) {
            char *reply_string = zframe_strdup (zmsg_last (reply));
            printf ("Reply: %s\n", reply_string);
            free (reply_string);
            zmsg_destroy (&reply);
 
            //  3. 关闭请求
            request = zmsg_new ();
            zmsg_add (request, zframe_dup (uuid));
            reply = s_service_call (session, "titanic.close", &request);
            zmsg_destroy (&reply);
            break;
        }
        else {
            printf ("I: 尚未收到应答,准备稍后重试...\n");
            zclock_sleep (5000);     //  5秒后重试
        }
    }
    zframe_destroy (&uuid);
    mdcli_destroy (&session);
    return 0;
}

        当然,上面的代码可以整合到一个框架中,程序员不需要了解其中的细节。如果我有时间的话,我会尝试写一个这样的API的,让应用程序又变回短短的几行。这种理念和MDP中的一致:不要做重复的事。

        下面是巨人的实现。这个服务端会使用三个线程来处理三种服务。它使用最原始的持久化方法来保存请求:为每个请求创建一个磁盘文件。虽然简单,但也挺恐怖的。比较复杂的部分是,巨人会维护一个队列来保存这些请求,从而避免重复地扫描目录。

        titanic: Titanic broker example in C

//
//  巨人模式 - 服务
//
//  实现 http://rfc.zeromq.org/spec:9 协议的服务端
 
//  让我们直接编译,不创建类库
#include "mdwrkapi.c"
#include "mdcliapi.c"
 
#include "zfile.h"
#include <uuid/uuid.h>
 
//  返回一个可打印的唯一编号(UUID)
//  调用者负责释放UUID字符串的内存
 
static char *
s_generate_uuid (void)
{
    char hex_char [] = "0123456789ABCDEF";
    char *uuidstr = zmalloc (sizeof (uuid_t) * 2 + 1);
    uuid_t uuid;
    uuid_generate (uuid);
    int byte_nbr;
    for (byte_nbr = 0; byte_nbr < sizeof (uuid_t); byte_nbr++) {
        uuidstr [byte_nbr * 2 + 0] = hex_char [uuid [byte_nbr] >> 4];
        uuidstr [byte_nbr * 2 + 1] = hex_char [uuid [byte_nbr] & 15];
    }
    return uuidstr;
}
 
//  根据UUID生成用于保存请求内容的文件名,并返回
 
#define TITANIC_DIR ".titanic"
 
static char *
s_request_filename (char *uuid) {
    char *filename = malloc (256);
    snprintf (filename, 256, TITANIC_DIR "/%s.req", uuid);
    return filename;
}
 
//  根据UUID生成用于保存应答内容的文件名,并返回
 
static char *
s_reply_filename (char *uuid) {
    char *filename = malloc (256);
    snprintf (filename, 256, TITANIC_DIR "/%s.rep", uuid);
    return filename;
}
 
 
//  ---------------------------------------------------------------------
//  巨人模式 - 请求服务
 
static void
titanic_request (void *args, zctx_t *ctx, void *pipe)
{
    mdwrk_t *worker = mdwrk_new (
        "tcp://localhost:5555", "titanic.request", 0);
    zmsg_t *reply = NULL;
 
    while (TRUE) {
        //  若应答非空则发送,再从代理处获得新的请求
        zmsg_t *request = mdwrk_recv (worker, &reply);
        if (!request)
            break;      //  中断并退出
 
        //  确保消息目录是存在的
        file_mkdir (TITANIC_DIR);
 
        //  生成UUID,并将消息保存至磁盘
        char *uuid = s_generate_uuid ();
        char *filename = s_request_filename (uuid);
        FILE *file = fopen (filename, "w");
        assert (file);
        zmsg_save (request, file);
        fclose (file);
        free (filename);
        zmsg_destroy (&request);
 
        //  将UUID加入队列
        reply = zmsg_new ();
        zmsg_addstr (reply, uuid);
        zmsg_send (&reply, pipe);
 
        //  将UUID返回给客户端
        //  将由循环顶部的mdwrk_recv()函数完成
        reply = zmsg_new ();
        zmsg_addstr (reply, "200");
        zmsg_addstr (reply, uuid);
        free (uuid);
    }
    mdwrk_destroy (&worker);
}
 
 
//  ---------------------------------------------------------------------
//  巨人模式 - 应答服务
 
static void *
titanic_reply (void *context)
{
    mdwrk_t *worker = mdwrk_new (
        "tcp://localhost:5555", "titanic.reply", 0);
    zmsg_t *reply = NULL;
 
    while (TRUE) {
        zmsg_t *request = mdwrk_recv (worker, &reply);
        if (!request)
            break;      //  中断并退出
 
        char *uuid = zmsg_popstr (request);
        char *req_filename = s_request_filename (uuid);
        char *rep_filename = s_reply_filename (uuid);
        if (file_exists (rep_filename)) {
            FILE *file = fopen (rep_filename, "r");
            assert (file);
            reply = zmsg_load (file);
            zmsg_pushstr (reply, "200");
            fclose (file);
        }
        else {
            reply = zmsg_new ();
            if (file_exists (req_filename))
                zmsg_pushstr (reply, "300"); //挂起
            else
                zmsg_pushstr (reply, "400"); //未知
        }
        zmsg_destroy (&request);
        free (uuid);
        free (req_filename);
        free (rep_filename);
    }
    mdwrk_destroy (&worker);
    return 0;
}
 
 
//  ---------------------------------------------------------------------
//  巨人模式 - 关闭请求
 
static void *
titanic_close (void *context)
{
    mdwrk_t *worker = mdwrk_new (
        "tcp://localhost:5555", "titanic.close", 0);
    zmsg_t *reply = NULL;
 
    while (TRUE) {
        zmsg_t *request = mdwrk_recv (worker, &reply);
        if (!request)
            break;      //  中断并退出
 
        char *uuid = zmsg_popstr (request);
        char *req_filename = s_request_filename (uuid);
        char *rep_filename = s_reply_filename (uuid);
        file_delete (req_filename);
        file_delete (rep_filename);
        free (uuid);
        free (req_filename);
        free (rep_filename);
 
        zmsg_destroy (&request);
        reply = zmsg_new ();
        zmsg_addstr (reply, "200");
    }
    mdwrk_destroy (&worker);
    return 0;
}
 
//  处理某个请求,成功则返回1
 
static int
s_service_success (mdcli_t *client, char *uuid)
{
    //  读取请求内容,第一帧为服务名称
    char *filename = s_request_filename (uuid);
    FILE *file = fopen (filename, "r");
    free (filename);
 
    //  如果client已经关闭了该请求,则返回1
    if (!file)
        return 1;
 
    zmsg_t *request = zmsg_load (file);
    fclose (file);
    zframe_t *service = zmsg_pop (request);
    char *service_name = zframe_strdup (service);
 
    //  使用MMI协议检查服务是否可用
    zmsg_t *mmi_request = zmsg_new ();
    zmsg_add (mmi_request, service);
    zmsg_t *mmi_reply = mdcli_send (client, "mmi.service", &mmi_request);
    int service_ok = (mmi_reply
        && zframe_streq (zmsg_first (mmi_reply), "200"));
    zmsg_destroy (&mmi_reply);
 
    if (service_ok) {
        zmsg_t *reply = mdcli_send (client, service_name, &request);
        if (reply) {
            filename = s_reply_filename (uuid);
            FILE *file = fopen (filename, "w");
            assert (file);
            zmsg_save (reply, file);
            fclose (file);
            free (filename);
            return 1;
        }
        zmsg_destroy (&reply);
    }
    else
        zmsg_destroy (&request);
 
    free (service_name);
    return 0;
}
 
 
int main (int argc, char *argv [])
{
    int verbose = (argc > 1 && streq (argv [1], "-v"));
    zctx_t *ctx = zctx_new ();
 
    //  创建MDP客户端会话
    mdcli_t *client = mdcli_new ("tcp://localhost:5555", verbose);
    mdcli_set_timeout (client, 1000);  //  1 秒
    mdcli_set_retries (client, 1);     //  只尝试一次
 
    void *request_pipe = zthread_fork (ctx, titanic_request, NULL);
    zthread_new (ctx, titanic_reply, NULL);
    zthread_new (ctx, titanic_close, NULL);
 
    //  主循环
    while (TRUE) {
        //  如果没有活动,我们将每秒循环一次
        zmq_pollitem_t items [] = { { request_pipe, 0, ZMQ_POLLIN, 0 } };
        int rc = zmq_poll (items, 1, 1000 * ZMQ_POLL_MSEC);
        if (rc == -1)
            break;              //  中断
        if (items [0].revents & ZMQ_POLLIN) {
            //  确保消息目录是存在的
            file_mkdir (TITANIC_DIR);
 
            //  将UUID添加到队列中,使用“-”号标识等待中的请求
            zmsg_t *msg = zmsg_recv (request_pipe);
            if (!msg)
                break;          //  中断
            FILE *file = fopen (TITANIC_DIR "/queue", "a");
            char *uuid = zmsg_popstr (msg);
            fprintf (file, "-%s\n", uuid);
            fclose (file);
            free (uuid);
            zmsg_destroy (&msg);
        }
        //  分派
        //
        char entry [] = "?.......:.......:.......:.......:";
        FILE *file = fopen (TITANIC_DIR "/queue", "r+");
        while (file && fread (entry, 33, 1, file) == 1) {
            //  处理UUID前缀为“-”的请求
            if (entry [0] == '-') {
                if (verbose)
                    printf ("I: 开始处理请求 %s\n", entry + 1);
                if (s_service_success (client, entry + 1)) {
                    //  标记为已处理
                    fseek (file, -33, SEEK_CUR);
                    fwrite ("+", 1, 1, file);
                    fseek (file, 32, SEEK_CUR);
                }
            }
            //  跳过最后一行
            if (fgetc (file) == '\r')
                fgetc (file);
            if (zctx_interrupted)
                break;
        }
        if (file)
            fclose (file);
    }
    mdcli_destroy (&client);
    return 0;
}

        测试时,打开mdbroker和titanic,再运行ticlient,然后开启任意个mdworker,就可以看到client获得了应答。

        几点说明:

                1、我们使用MMI协议去向代理询问某项服务是否可用,这一点和MDP中的逻辑一致;

                2、我们使用inproc(进程内)协议建立主循环和titanic.request服务间的联系,保存新的请求信息。这样可以避免主循环不断扫描磁盘目录,读取所有请求文件,并按照时间日期排序。

        这个示例程序不应关注它的性能(一定会非常糟糕,虽然我没有测试过),而是应该看到它是如何提供一种可靠的通信模式的。你可以测试一下,打开代理、巨人、worker和client,使用-v参数显示跟踪信息,然后随意地开关代理、巨人、或worker(client不能关闭),可以看到所有的请求都能获得应答。

        如果你想在真实环境中使用巨人模式,你肯定会问怎样才能让速度快起来。以下是我的做法:

                1、使用一个磁盘文件保存所有数据。操作系统处理大文件的效率要比处理许多小文件来的高。

                2、使用一种循环的机制来组织该磁盘文件的结构,这样新的请求可以被连续地写入这个文件。单个线程在全速写入磁盘时的效率是比较高的。

                3、将索引保存在内存中,可以在启动程序时重建这个索引。这样做可以节省磁盘缓存,让索引安全地保存在磁盘上。你需要用到fsync的机制来保存每一条数据;或者可以等待几毫秒,如果不怕丢失上千条数据的话。

                4、如果条件允许,应选择使用固态硬盘;

                5、提前分配该磁盘文件的空间,或者将每次分配的空间调大一些,这样可以避免磁盘碎片的产生,并保证读写是连续的。

        另外,我不建议将消息保存在数据库中,甚至不建议交给那些所谓的高速键值缓存,它们比起一个磁盘文件要来得昂贵。

        如果你想让巨人模式变得更为可靠,你可以将请求复制到另一台服务器上,这样就不需要担心主程序遭到核武器袭击了。

        如果你想让巨人模式变得更为快速,但可以牺牲一些可靠性,那你可以将请求和应答都保存在内存中。这样做可以让该服务作为脱机网络运行,不过若巨人服务本身崩溃了,我也无能为力。

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

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

相关文章

【JS】数据结构之栈

文章目录基本介绍代码实现基本介绍 内存中的堆栈和数据机构中的堆栈不是一个概念&#xff0c;内存中的堆栈是真实存在的物理区&#xff0c;数据结构中的堆栈是抽象数据存储结构。 栈&#xff1a;是一种受限制的线性表。他遵循后进先出的原则&#xff08;LIFO&#xff09;其限制…

神仙级编程神器,吹爆

Visual Studio 编程领域公认的“最强IDE”&#xff0c;Visual Studio是目前最流行的Windows平台应用程序的集成开发环境&#xff0c;提供了高级开发工具、调试功能、数据库功能和创新功能&#xff0c;帮助在各种平台上快速创建当前最先进的应用程序&#xff0c;开发新的程序。 …

【ODX介绍】-5-用于Flash刷写的ODX-F文件概述

总目录:(单击下方链接皆可跳转至专栏总目录) 《UDS/OBD诊断需求编辑工具》总目录https://blog.csdn.net/qfmzhu/article/details/123697014 共9页精讲:在第二章节中,附上了一个完整的,且详细的ODX-F文件层级结构图。 目录 1 什么是ODX-F?

【在Spring MVC框架和Spring Boot项目中,控制器的响应结果】

目录 1. 控制器的响应结果 2. 相关配置 3. 使用枚举优化代码 1. 控制器的响应结果 当控制器处理了请求之后&#xff0c;向客户端响应的结果中&#xff0c;应该至少包含&#xff1a; 业务状态码&#xff1a;通常是数值类型的&#xff0c;客户端可以根据此数值来判断操作成功…

docke部署nodejs程序及Dockerfile详解

目录参考一、Dockerfile二、部署1、程序结构2、新建Dockerfile3、新建.dockerignore4、构建镜像5、创建容器6、关闭镜像参考 重点参考&#xff1a;把一个 Node.js web 应用程序给 Docker 化 Docker部署Node.js的方法步骤&#xff08;nodejs docker部署&#xff09; 一、Docke…

Linux服务器启动tomcat的三种方式

直接进入主题&#xff0c;首先cd进入tomcat的bin文件夹下&#xff0c;然后可以尝试以下三种启动方式&#xff1a; 第一种&#xff08;当前会话启动&#xff09;&#xff1a; ./startup.sh 效果: 然后tomcat就在后台启动了&#xff0c;我们还可以在当前会话中继续输入其它指令…

PHP基于thinkphp的网上书店管理系统#毕业设计

本论文主要论述了如何使用php语言开发一个网上图书管理系统&#xff0c;本系统将严格按照软件开发流程进行各个阶段的工作&#xff0c;面向对象编程思想进行项目开发。在引言中&#xff0c;将论述网上图书管理系统的当前背景以及系统开发的目的&#xff0c;后续章节将严格按照软…

【python】 16进制字符串转list

def splitStringToByteList(bytesString): # 拆分字符串成字节列表bytesList []for i in range(int(len(bytesString)/2)):bytesList.append(bytesString[i*2:i*22])return bytesListif __name__ __main__:print(splitStringToByteList("1E1E2AEB4ACC4C")) 结果&…

shiro key文件

​下面结合实战以及shiro的CookieRememberMeManaer的调用过程,浅谈获取shiro key文件的几种方式。 shiro key文件的获取方式:1结合Dnslog与URLDNS;2利用时间延迟或报错;3结合CookieRememberMeManaer 1结合Dnslog与URLDNS 在进行漏洞探测的时候,一般会使用ysoserial-URL…

Codeforces Round #835 (Div. 4)A.B.C.D.E.F

A. Medium Number 题目链接&#xff1a; Problem - A - Codeforces 题面&#xff1a; 题意&#xff1a; 给定三个数&#xff0c;求中间那个数的值 思路&#xff1a; 我们可以分别求出三个数的总和&#xff0c;最大值和最小值&#xff0c;在通过总和减最大值和最小值的方…

Promise(微任务)- 让你看完就懂

1. 图示 思维导图链接 https://www.zhixi.com/view/23ff2291 2. 使用promise原因 在没有promise的时候&#xff0c;一直使用setTimeout函数&#xff0c;这样就会造成回调地狱。 3. 基本状态 promise 有三种状态 pending&#xff08;此时 promise还没有调用完成&#xff09…

改进花朵授粉算法的无线传感器网络部署优化(Matlab代码实现)

&#x1f352;&#x1f352;&#x1f352;欢迎关注&#x1f308;&#x1f308;&#x1f308; &#x1f4dd;个人主页&#xff1a;我爱Matlab &#x1f44d;点赞➕评论➕收藏 养成习惯&#xff08;一键三连&#xff09;&#x1f33b;&#x1f33b;&#x1f33b; &#x1f34c;希…

某汽车座椅进行的头冲吸能实验的仿真计算

本案例运用hypermesh和lsdyna联合仿真&#xff0c;主要是针对某座椅进行的头冲吸能实验的仿真计算。这个工况考察座椅背部的塑料件的破坏情况&#xff0c;以及头部模块的加速度情况&#xff0c;达到保护人头部的效果。 本案例用户可以学习到&#xff1a; 1、螺栓预紧力的施加…

IntelliJ IDEA + spring-boot+mysql简单实现获取数据库数据接口例子

一、新建一个spring-boot项目 demo 项目结构与文件的含义&#xff1a; 二、项目编译入口代码 DemoApplication.java SpringBootApplication //EnableAutoConfiguration(exclude{DataSourceAutoConfiguration.class}) public class DemoApplication {public static void main(…

Linux系统编程

Linux系统中所见皆文件 bin&#xff1a;所存放二进制可执行文件 boot&#xff1a;存放开机启动程序 dev&#xff1a;存放设备文件 home&#xff1a;存放用户 etc&#xff1a;用户信息和系统配置文件 lib&#xff1a;库文件 root&#xff1a;管理员宿主目录&#xff08;家目录&…

Linux计划任务管理,网络管理

一&#xff0c;计划任务管理&#xff1a; 任务管理很宽泛&#xff0c;这里是指的计划任务管理&#xff0c;在指定的时间执行。 at命令 &#xff1a; 由atd守护进程来执行&#xff0c;atd进程会定期检查系统上的 /var/spool/at 目录&#xff0c;获取at命令写入的任务。 &#x…

Linux CentOS系统安装mysql8.0.31(tar方式)

Linux系统安装mysql8.0.31&#xff08;TAR方式&#xff09; 文章目录Linux系统安装mysql8.0.31&#xff08;TAR方式&#xff09;安装包的下载安装Mysql8.0.31注册成系统服务并自启动问题总结启动异常一简单使用安装包的下载 选择的是 MySQL Community Server 进行下载 官网下…

第4季1:将AR0130摄像头更换为OV9712摄像头

以下内容源于朱有鹏嵌入式课程的学习与整理&#xff0c;如有侵权请告知删除。 一、SoC对Sensor的支持情况 在海思SDK的Hi3518E V200R001C01SPC030\00.hardware\chip\document_cn目录下&#xff0c;我们重点关注这两个文档&#xff1a;《Hi3518EV200经济型HD IP Camera SoC产品…

wordpress插件-wordpress蜘蛛记录插件

wordpress蜘蛛插件&#xff0c;我们为什么要wordpress装蜘蛛插件&#xff0c;一个合格的SEO人员必须懂得分析网站的蜘蛛情况&#xff0c;wordpress蜘蛛插件可以让我们分析出各大搜索引擎蜘蛛的访问间隔频率&#xff0c;以及最受蜘蛛喜爱的页面&#xff0c;让你精确地分析搜索引…

Unity DOTS学习 前置知识(二)

ECS 架构和一些专用术语 Entity-Component-System 实体组件系统(ECS )架构遵循组合优于继承的原则面向数据设计弱耦合常被应用在游戏开发上 本质&#xff1a;组合的是数据数组而非对象数组 ECS示例图&#xff1a; Entity 虽然叫做实体&#xff0c;但是并非对象或容器&…