wenmo8 发布的文章

开心一刻

上午,走路不小心踩了钉子,去打了破伤风
下午,又特么踩到了钉子,我问医生
我:还需要打针吗
医生:你有那钱还是看看眼睛吧

宋小宝_红帽大笑

基础回顾

项目基于
xxl-job 2.1.0
实现的分布式调度,所以我还是基于此来模拟实现我们的项目;关于
XXL-JOB
,本文不做额外介绍,大家可以去看官网

https://www.xuxueli.com/xxl-job/

或者我之前的博客

分布式任务调度平台 → XXL-JOB 初探
分布式任务调度平台 → XXL-JOB 实战
当 xxl-job 遇上 docker → 它晕了,我也乱了!
当 xxl-job 遇上 docker → 它晕了,但我不能乱!

XXL-JOB
分两块:
调度中心

执行器

xxl-job架构图

其中
调度中心
就是
xxl-job-admin
,已经是一个完善的模块,直接部署使用即可;而
执行器
,也就是任务执行器,需要跟我们的业务代码绑定,完成我们的业务逻辑,我们一个一个来部署

  1. 调度中心

    xxl-job-admin 有自己的表,官方提供了初始化 SQL:
    tables_xxl-job.sql

    CREATE database if NOT EXISTS `xxl_job` default character set utf8 collate utf8_general_ci;
    use `xxl_job`;
    
    
    CREATE TABLE `xxl_job_info` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `job_group` int(11) NOT NULL COMMENT '执行器主键ID',
      `job_cron` varchar(128) NOT NULL COMMENT '任务执行CRON',
      `job_desc` varchar(255) NOT NULL,
      `add_time` datetime DEFAULT NULL,
      `update_time` datetime DEFAULT NULL,
      `author` varchar(64) DEFAULT NULL COMMENT '作者',
      `alarm_email` varchar(255) DEFAULT NULL COMMENT '报警邮件',
      `executor_route_strategy` varchar(50) DEFAULT NULL COMMENT '执行器路由策略',
      `executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
      `executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数',
      `executor_block_strategy` varchar(50) DEFAULT NULL COMMENT '阻塞处理策略',
      `executor_timeout` int(11) NOT NULL DEFAULT '0' COMMENT '任务执行超时时间,单位秒',
      `executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
      `glue_type` varchar(50) NOT NULL COMMENT 'GLUE类型',
      `glue_source` mediumtext COMMENT 'GLUE源代码',
      `glue_remark` varchar(128) DEFAULT NULL COMMENT 'GLUE备注',
      `glue_updatetime` datetime DEFAULT NULL COMMENT 'GLUE更新时间',
      `child_jobid` varchar(255) DEFAULT NULL COMMENT '子任务ID,多个逗号分隔',
      `trigger_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '调度状态:0-停止,1-运行',
      `trigger_last_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '上次调度时间',
      `trigger_next_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '下次调度时间',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    CREATE TABLE `xxl_job_log` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `job_group` int(11) NOT NULL COMMENT '执行器主键ID',
      `job_id` int(11) NOT NULL COMMENT '任务,主键ID',
      `executor_address` varchar(255) DEFAULT NULL COMMENT '执行器地址,本次执行的地址',
      `executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
      `executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数',
      `executor_sharding_param` varchar(20) DEFAULT NULL COMMENT '执行器任务分片参数,格式如 1/2',
      `executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
      `trigger_time` datetime DEFAULT NULL COMMENT '调度-时间',
      `trigger_code` int(11) NOT NULL COMMENT '调度-结果',
      `trigger_msg` text COMMENT '调度-日志',
      `handle_time` datetime DEFAULT NULL COMMENT '执行-时间',
      `handle_code` int(11) NOT NULL COMMENT '执行-状态',
      `handle_msg` text COMMENT '执行-日志',
      `alarm_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '告警状态:0-默认、1-无需告警、2-告警成功、3-告警失败',
      PRIMARY KEY (`id`),
      KEY `I_trigger_time` (`trigger_time`),
      KEY `I_handle_code` (`handle_code`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    CREATE TABLE `xxl_job_logglue` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `job_id` int(11) NOT NULL COMMENT '任务,主键ID',
      `glue_type` varchar(50) DEFAULT NULL COMMENT 'GLUE类型',
      `glue_source` mediumtext COMMENT 'GLUE源代码',
      `glue_remark` varchar(128) NOT NULL COMMENT 'GLUE备注',
      `add_time` timestamp NULL DEFAULT NULL,
      `update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    CREATE TABLE `xxl_job_registry` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `registry_group` varchar(255) NOT NULL,
      `registry_key` varchar(255) NOT NULL,
      `registry_value` varchar(255) NOT NULL,
      `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
      PRIMARY KEY (`id`),
      KEY `i_g_k_v` (`registry_group`,`registry_key`,`registry_value`),
      KEY `i_u` (`update_time`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    CREATE TABLE `xxl_job_group` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `app_name` varchar(64) NOT NULL COMMENT '执行器AppName',
      `title` varchar(12) NOT NULL COMMENT '执行器名称',
      `order` tinyint(4) NOT NULL DEFAULT '0' COMMENT '排序',
      `address_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '执行器地址类型:0=自动注册、1=手动录入',
      `address_list` varchar(512) DEFAULT NULL COMMENT '执行器地址列表,多地址逗号分隔',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    CREATE TABLE `xxl_job_user` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `username` varchar(50) NOT NULL COMMENT '账号',
      `password` varchar(50) NOT NULL COMMENT '密码',
      `role` tinyint(4) NOT NULL COMMENT '角色:0-普通用户、1-管理员',
      `permission` varchar(255) DEFAULT NULL COMMENT '权限:执行器ID列表,多个逗号分割',
      PRIMARY KEY (`id`),
      UNIQUE KEY `i_username` (`username`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    CREATE TABLE `xxl_job_lock` (
      `lock_name` varchar(50) NOT NULL COMMENT '锁名称',
      PRIMARY KEY (`lock_name`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    
    INSERT INTO `xxl_job_group`(`id`, `app_name`, `title`, `order`, `address_type`, `address_list`) VALUES (1, 'xxl-job-executor-sample', '示例执行器', 1, 0, NULL);
    INSERT INTO `xxl_job_info`(`id`, `job_group`, `job_cron`, `job_desc`, `add_time`, `update_time`, `author`, `alarm_email`, `executor_route_strategy`, `executor_handler`, `executor_param`, `executor_block_strategy`, `executor_timeout`, `executor_fail_retry_count`, `glue_type`, `glue_source`, `glue_remark`, `glue_updatetime`, `child_jobid`) VALUES (1, 1, '0 0 0 * * ? *', '测试任务1', '2018-11-03 22:21:31', '2018-11-03 22:21:31', 'XXL', '', 'FIRST', 'demoJobHandler', '', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化', '2018-11-03 22:21:31', '');
    INSERT INTO `xxl_job_user`(`id`, `username`, `password`, `role`, `permission`) VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 1, NULL);
    INSERT INTO `xxl_job_lock` ( `lock_name`) VALUES ( 'schedule_lock');
    
    commit;
    

    我们先通过此 SQL 把库、表创建好


    admin库表

    官方提供了 Docker 镜像,我们直接用 Docker 来部署


    docker run -d -e PARAMS="--spring.datasource.url=jdbc:mysql://192.168.2.118:3312/xxl_job?Unicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
    --spring.datasource.username=root
    --spring.datasource.password=123456
    --xxl.job.accessToken=qsl.token"
    -p 8080:8080
    --name xxl-job-admin --restart=always xuxueli/xxl-job-admin:2.1.0


    不出意外的话,xxl-job-admin 就部署成功了


    admin部署成功
  2. 执行器

    为了方便演示,直接在本地 IDEA 中启动;我们基于
    Spring Boot
    搭建,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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.qsl</groupId>
        <artifactId>spring-boot-xxl-job-executor</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <properties>
            <maven.compiler.source>8</maven.compiler.source>
            <maven.compiler.target>8</maven.compiler.target>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        </properties>
    
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.7.18</version>
        </parent>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <exclusions>
                    <exclusion>
                        <artifactId>spring-boot-starter-logging</artifactId>
                        <groupId>org.springframework.boot</groupId>
                    </exclusion>
                </exclusions>
            </dependency>
    
            <dependency>
                <groupId>com.xuxueli</groupId>
                <artifactId>xxl-job-core</artifactId>
                <version>2.1.0</version>
            </dependency>
    
            <dependency>
                <groupId>ch.qos.logback</groupId>
                <artifactId>logback-classic</artifactId>
            </dependency>
        </dependencies>
    
    </project>
    

    配置文件:application.yml

    xxl:
      job:
        executor:
          appName: qsl-job-executor
          admin-addresses: http://192.168.2.118:8080/xxl-job-admin
          access-token: qsl.token
    

    xxl-job spring executor:XxlJobSpringExecutor

    /**
     * @author 青石路
     */
    @Configuration
    public class XxlJobConfig {
    	private final Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);
    
    	@Bean(initMethod = "start",destroyMethod = "destroy")
    	public XxlJobSpringExecutor xxlJobExecutor(XxlJobProperties xxlJob) {
    		logger.info(">>>>>>>>>>> xxl-job config init.");
    		XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
    		xxlJobSpringExecutor.setAdminAddresses(xxlJob.getAdminAddresses());
    		xxlJobSpringExecutor.setAppName(xxlJob.getAppName());
    		xxlJobSpringExecutor.setIp(xxlJob.getIp());
    		xxlJobSpringExecutor.setPort(xxlJob.getPort());
    		xxlJobSpringExecutor.setAccessToken(xxlJob.getAccessToken());
    		xxlJobSpringExecutor.setLogPath(xxlJob.getLogPath());
    		xxlJobSpringExecutor.setLogRetentionDays(xxlJob.getLogRetentionDays());
    		return xxlJobSpringExecutor;
    	}
    }
    

    最后剩一个 JobHandler:QslJobHandler

    /**
     * @author 青石路
     */
    @Component
    @JobHandler("qslJobHandler")
    public class QslJobHandler extends IJobHandler {
    
        private static final Logger LOG = LoggerFactory.getLogger(QslJobHandler.class);
    
        @Override
        public ReturnT<String> execute(String param) throws Exception {
            LOG.info("param = {}", param);
            // TODO 业务处理
            return ReturnT.SUCCESS;
        }
    }
    

    代码完整地址:
    spring-boot-xxl-job-executor

    我们启动执行器,不出意外的话,执行器会启动成功


    executor启动成功

调度中心

执行器
都启动成功后,还需要在调度中心注册执行器和添加任务,页面操作即可

  1. 注册执行器


    新增执行器

    这里的
    AppName
    不能随意填写,需要与 application.yml 中

    xxl:
      job:
        executor:
          appName: qsl-job-executor
    

    配置的值保持一致,即填写:
    qsl-job-executor

    名称

    排序
    可以随意填,推荐填的有业务含义,特别是名称,最好见名知意

    注册方式
    采用
    手动录入
    的方式,因为调度中心是采用 Docker 部署的,而执行器是通过本地 IDEA 启动的,自动注册会导致调度中心与执行器之间网络不通

    机器地址
    填本地 IDEA 所在机器的 IP,端口是
    9999
    ,而非
    8080
    ,所以完整地址是:
    192.168.2.13:9999
    ;多个地址间用英文逗号隔开即可

    最后点击
    保存
    即可


    执行器列表
  2. 添加任务


    新增任务

    如上的任务参数,我都是按我们项目的生产参数来配置的,每个参数的详细说明,大家可以去看 XXL-JOB 官方的
    任务详解

至此就算全部配置完成,我们执行下任务试试

执行任务测试

执行结果如下

执行任务测试成功

IDEA 控制台输出如下

执行任务测试成功_控制台输出

一切似乎都是那么正常

线上问题

我们的生产环境是基于
k8s
部署的,当执行器的内存使用率占用分配内存的 90% 时,会触发 k8s 的重启机制;因为不好模拟生产环境,所以我们需要调整下
QslJobHandler
来模拟线上问题

/**
 * @author 青石路
 */
@Component
@JobHandler("qslJobHandler")
public class QslJobHandler extends IJobHandler {

    private static final Logger LOG = LoggerFactory.getLogger(QslJobHandler.class);

    @Override
    public ReturnT<String> execute(String param) throws Exception {
        LOG.info("param = {}", param);

        LOG.info("QslJobHandler 业务处理开始");
        // TODO 业务处理
        TimeUnit.SECONDS.sleep(30);
        LOG.info("QslJobHandler 业务处理完成");
        return ReturnT.SUCCESS;
    }
}

任务执行的过程中(即业务处理中,还未处理完成),我们重启下执行器

job还未执行完重启执行器

模拟的是任务执行的时候,执行器
OOM
导致内存使用率超过 90%,触发 k8s 的重启机制,导致执行器重启;那么有什么问题呢,我们来看下调度日志

重启_调度日志

这个数据,你们都能看懂吧,因为任务在执行过程中,执行器重启导致没有给调度中心进行响应,所以调度中心一直在苦苦等待执行器的回信,殊不知执行器已喝孟婆汤完成了转生

我都到花儿都谢了

那这对我们的业务有什么影响吗,肯定有影响,因为我们配置的 cron(
0 50 5 * * ?
)是每天只执行一次,这次没执行成功,今天就不会被调度执行了,然后我们就喜提一个
生产事故

从根因上来讲,罪魁祸首是 OOM,但一轮排查下来,发现这个吊毛隐藏的挺深,加上又是偶发一次,一时半会还真查不出来 OOM 的原因;所以我们得想办法对这种偶发问题进行弥补,如果是上班时间,我们可以根据重启告警进行人工弥补调度,但如果不是上班时间呢,甚至是半夜呢,所以我们应该通过程序去自动弥补调度,人工弥补调度只是最后的兜底;这个时候如果调度中心能够监测出哪些任务需要弥补调度并自动进行弥补,那就美滋滋了!

期望自动弥补

调度中心如何监测呢,是不是可以通过
任务超时时间
来实现?如果指定时间内,执行器没有返回任务执行结果,调度中心就认为任务执行失败了,重新发送一次调度请求,然后结合
失败重试次数
来避免无限重试;而 XXL-JOB 正好提供了这两个配置,其说明如下

任务超时时间与失败重试次数说明

针对
失败重试
,官方还有这样一段说明

失败重试生效阶段

这两段说明结合起来看,不知道你们有没有什么想法,我心中倒是涌起了一股不祥的预感

我前面说的美好预想都是基于
任务超时时间
是在
调度中心
实现的,或者说
调度中心

执行器
都有实现,只有这样,调度中心才能实现监测;但
主动中断任务

调度 + 执行
这些字眼,让我觉得
调度中心
不会实现
任务超时时间

执行器
才会实现它

至于 XXL-JOB 具体是如何实现的,我们一测便知

任务超时时间与失败重试次数配置

然后重复之前执行器的重启过程(调度请求到了执行器后,立马重启执行器),我们再来看看执行结果

重启不算超时

结果显示任务并未终止,调度中心也没有按我们预期的那样就行重试,我的不祥预感成真了!

任务超时时间
只在
执行器
端做了实现,精确的控制任务执行时长,并及时的进行超时中断
调度重试次数
在 调度 与 执行 阶段都可以生效,前提是触发了重试机制,重试的方式是再次触发调度

当我们去手动
终止任务
,调度中心过几秒后会进行重试

失败重试记录

点击调度备注的查看,可以看到任务触发类型是:失败重试触发

任务触发类型

所以结论是

XXL-JOB 有任务执行超时后的重试功能,但前提是执行器不能重启
这也得出了标题的答案:
执行中的任务在执行器重启后不会自动弥补调度

总结

  1. 上述的结论都是基于 XXL-JOB 2.1.0,不一定适配其他版本


    XXL-JOB 有任务执行超时后的重试功能,但前提是执行器不能重启;执行中的任务在执行器重启后不会自动弥补调度


    也许 XXL-JOB 在 2.1.0 之后的某个版本已经满足标题的需求,或者在未来的版本中会支持,但不会是 2.1.0

  2. 针对标题中的问题,只能先加重启监控进行人工弥补,同时去找 OOM 的原因并进行修复

  3. 关于
    任务超时控制
    ,官方特意强调了一些注意点


    任务超时控制

    另外,也推荐大家去查看下
    4.9 终止运行中的任务

  4. 示例代码:
    spring-boot-xxl-job-executor

技术背景

在前面的
一篇博客
中,我们介绍过PySAGES这个增强采样软件的基本安装和使用方法。该软件类似于Plumed是一个外挂增强采样软件,但是PySAGES是基于Python语言和Jax框架来实现的,在性能上有一定的优势。这里我们结合PySAGES的易开发特性,和
CUDA SPONGE
的高性能特性,做一个简单的扩展将二者联合起来进行分子动力学模拟与增强采样。

耦合框架

在前面的文章中我们介绍过SPONGE的Python接口以及调用模式:

这里再总结一个PySAGES的基本调用模式:

目前PySAGES已经集成了一些MD Backend,例如前面介绍过的
OpenMM
,以及基于Jax框架开发的Jax-MD等等。这些框架都可以通过Extension模块跟PySAGES进行对接,其中Jax-MD的Extension是在PySAGES中直接定义的,因为两者都基于Jax,底层兼容性较好。而OpenMM的Extension单独出来一个叫openmm_dlext的扩展包,这是因为其底层基于cupy开发,需要通过dlpack这个标准化工具在GPU上进行免拷贝操作。比较遗憾的是,目前MindSpore暂不支持dlpack。

PySAGES大概的模拟流程是这样的,先在MD Backend中定义好力场和积分器,将输入坐标传递给Extension再到PySAGES中构建SnapShot、SnapShot Method和Helper。然后在PySAGES中定义增强采样Method,例如MetaDynamics Method,传入构建好的SnapShot和Helper,就可以得到一个用于更新的函数Update Function和一个State增强采样状态参量。到这里PySAGES的初始化就结束了。然后在MD Backend中计算好Force,把相应的Force传给PySAGES进行更新,PySAGES的Update Function接收一个State和一个Snap就可以得到新的State,这个State中有一个bias变量,就是偏置势对应的Bias Force。把Bias Force直接加到MD Backend中传过来的Force中,就可以得到一个全新的Force用于积分器的迭代。

PySAGES与CUDA SPONGE

接上一个章节内容,这里需要特别说明一下CUDA SPONGE提供的Python接口与PySAGES的运用方法。因为CUDA SPONGE的调用形式是通过CUDA去调用Python的函数内容,而PySAGES的调用形式是从Python去控制特定Backend的CUDA函数的句柄,来实现二者的结合。一个以CUDA为主,一个以Python为主。那么要结合的话,使用CUDA Sponge自身的调用形式会相对容易一些,因为我们将PySAGES中仅当作一个用于计算Bias Force的模块,集成到SPONGE的运行流程中。当然,为了结合PySAGES,我们还是需要手动去定义一些SPONGE的SnapShot和Helper,首先是SnapShot:

from pysages.backends.snapshot import Snapshot

def build_sponge_snapshot(num_atoms):
    crd = jnp.array(np.random.random((num_atoms, 3)), jnp.float32)
    ids = jnp.arange(num_atoms)
    forces = jnp.zeros_like(crd)
    return Snapshot(crd, None, forces, ids, None, None, None)

这里因为是初始化,所以可以直接用一个jax的随机array来构造。其实也可以直接传一个backend的crd进来,但是为了区分initialization和update的功能,这里还是直接随机生成了一个。这里的SnapShot是一个namedtuple格式,存储了坐标和力等系统信息,这里为了最简化,我们只传入三个最基本的参数:坐标、力、原子索引。然后是SnapShotMethod和Helper,这两个标准化接口用于实时获取SnapShot中的信息:

from pysages.backends.snapshot import SnapshotMethods, HelperMethods

def build_sponge_snapshot_methods():
    def positions(snapshot):
        return snapshot.positions
    def indices(snapshot):
        return snapshot.ids
    return SnapshotMethods(positions, indices, None, None)

def build_sponge_helper(dims=3):
    def get_dims():
        return dims
    return HelperMethods(build_data_querier(build_sponge_snapshot_methods(), {"positions", "indices"}), get_dims)

这样我们就完成了一个基本的SPONGE的Extension的构建。

完整示例

这里是一个把CUDA SPONGE作为MD Backend,然后用PySAGES来进行增强采样的一个简单示例:

"""
SPONGE Usage:
    $ ../SPONGE -mdin nvt.txt
"""
import Sponge

Sponge.controller.Step_Print_Initial("Phi", "%2f")
Sponge.controller.Step_Print_Initial("Psi", "%2f")

import pysages
from pysages.colvars import DihedralAngle
from numpy import pi
from pysages.methods import Metadynamics
from pysages.backends.snapshot import Snapshot, SnapshotMethods, HelperMethods, build_data_querier

import numpy as np
from jax import numpy as jnp
from jax.dlpack import to_dlpack, from_dlpack
from cupy import fromDlpack as cufd

kB = 0.00831446261815324
T = 300

def build_sponge_snapshot_methods():
    def positions(snapshot):
        return snapshot.positions
    def indices(snapshot):
        return snapshot.ids
    return SnapshotMethods(positions, indices, None, None)

def build_sponge_snapshot(num_atoms):
    crd = jnp.array(np.random.random((num_atoms, 3)), jnp.float32)
    ids = jnp.arange(num_atoms)
    forces = jnp.zeros_like(crd)
    return Snapshot(crd, None, forces, ids, None, None, None)

def build_sponge_helper(dims=3):
    def get_dims():
        return dims
    return HelperMethods(build_data_querier(build_sponge_snapshot_methods(), {"positions", "indices"}), get_dims)

# 定义增强采样方法
def phi_psi():
    cvs = [DihedralAngle([4, 6, 8, 14]), DihedralAngle([6, 8, 14, 16])]
    height = 5.0  # kJ/mol
    sigma = [0.4, 0.4]  # radians
    stride = 3
    ngauss = 500
    grid = pysages.Grid(lower=(-pi, -pi), upper=(pi, pi), shape=(50, 50), periodic=True)
    method = Metadynamics(cvs, height, sigma, stride, ngauss, grid=grid)
    return method

initialized = False
state = None
snap = None
helper = None
method = None
update_func = None

# SPONGE用于更新Force的标准化调用
def Calculate_Force():
    global initialized
    global state
    global snap
    global helper
    global method
    global update_func

    if not initialized:
        initialized = True
        num_atoms = Sponge.md_info.frc.shape[0]
        snap = build_sponge_snapshot(num_atoms)
        helper = build_sponge_helper()
        method = phi_psi()
        res = method.build(snap, helper)
        state = res[1]()
        update_func = res[2]
    
    snap = snap._replace(positions=from_dlpack(Sponge.md_info.crd.toDlpack()))
    state = update_func(snap, state)
    # 加Meta
    Sponge.md_info.frc += cufd(to_dlpack(state.bias))
    # 不加Meta
    # Sponge.md_info.frc += 0

# 手动记录CV值
import os
record_name = "cv_record.txt"
if os.path.exists(record_name):
    os.remove(record_name)

# SPONGE的标准化打印输出接口
def Mdout_Print():
    global state
    global record_name
    Sponge.controller.Step_Print("Phi", state.xi[0][0])
    Sponge.controller.Step_Print("Psi", state.xi[0][1])
    with open(record_name, 'a+') as file:
        file.write("{},{}\n".format(state.xi[0][0], state.xi[0][1]))

其中用到的SPONGE配置文件为:

case1 MD simulation

mode = NVT
default_in_file_prefix = protein/alad

pbc=0 
cutoff=999

dt = 1e-3
step_limit = 5000
write_information_interval = 10

thermostat = middle_langevin
middle_langevin_gamma = 10

rst = nvt_restart

coordinate_in_file = protein/alad_coordinate.txt
plugin = /usr/local/python-3.7.5/lib/python3.7/site-packages/prips/_prips.so
py = pysages_test.py

原始的pdb文件为:

ATOM      1  H1  ACE     1       2.000   1.000  -0.000  1.00  0.00
ATOM      2  CH3 ACE     1       2.000   2.090   0.000  1.00  0.00
ATOM      3  H2  ACE     1       1.486   2.454   0.890  1.00  0.00
ATOM      4  H3  ACE     1       1.486   2.454  -0.890  1.00  0.00
ATOM      5  C   ACE     1       3.427   2.641  -0.000  1.00  0.00
ATOM      6  O   ACE     1       4.391   1.877  -0.000  1.00  0.00
ATOM      7  N   ALA     2       3.555   3.970  -0.000  1.00  0.00
ATOM      8  H   ALA     2       2.733   4.556  -0.000  1.00  0.00
ATOM      9  CA  ALA     2       4.853   4.614  -0.000  1.00  0.00
ATOM     10  HA  ALA     2       5.408   4.316   0.890  1.00  0.00
ATOM     11  CB  ALA     2       5.661   4.221  -1.232  1.00  0.00
ATOM     12  HB1 ALA     2       5.123   4.521  -2.131  1.00  0.00
ATOM     13  HB2 ALA     2       6.630   4.719  -1.206  1.00  0.00
ATOM     14  HB3 ALA     2       5.809   3.141  -1.241  1.00  0.00
ATOM     15  C   ALA     2       4.713   6.129   0.000  1.00  0.00
ATOM     16  O   ALA     2       3.601   6.653   0.000  1.00  0.00
ATOM     17  N   NME     3       5.846   6.835   0.000  1.00  0.00
ATOM     18  H   NME     3       6.737   6.359  -0.000  1.00  0.00
ATOM     19  CH3 NME     3       5.846   8.284   0.000  1.00  0.00
ATOM     20 HH31 NME     3       4.819   8.648   0.000  1.00  0.00
ATOM     21 HH32 NME     3       6.360   8.648   0.890  1.00  0.00
ATOM     22 HH33 NME     3       6.360   8.648  -0.890  1.00  0.00
TER   
END   

使用Xponge根据pdb文件进行建模的方法,可以参考
这篇文章
中的流程。需要注意的是,生成一个坐标文件之后,需要手动把坐标文件里面的Box信息(最后一行的前三列)修改为
999
(不加PBC Box的情况下),改完大概是这样的一个txt文件:

22
3.514000 3.000000 5.288678
3.514000 4.090000 5.288679
3.000000 4.454000 6.021000
3.000000 4.454000 4.241000
4.941000 4.641000 5.288676
5.905000 3.877000 5.288673
5.069000 5.970000 5.288679
4.247000 6.556000 5.288679
6.367000 6.614000 5.288679
6.922000 6.316000 6.021000
7.175000 6.221000 3.899000
6.637000 6.521000 3.000000
8.144000 6.719000 3.925000
7.323000 5.141000 3.890000
6.227000 8.129000 5.288675
5.115000 8.653000 5.288673
7.360000 8.835000 5.288687
8.251000 8.359000 5.288693
7.360000 10.284000 5.288682
6.333000 10.648000 5.288674
7.874000 10.648000 6.021000
7.874000 10.648000 4.241000
999 999 999 90.000000 90.000000 90.000000

在确保SPONGE和PySAGES两者都正常安装的情况下,如果不加Meta,跑出来的CV轨迹是这样的:

加上Meta之后,CV轨迹是这样的:

可以看到,我们加上的Meta明显提升了MD的采样空间。

其中作图脚本如下:

import numpy as np
import matplotlib.pyplot as plt

def gaussian2(x1, x2, sigma1=1.0, sigma2=1.0, A=0.5):
    return np.sum(A*np.exp(-0.5*(x1**2/sigma1**2+x2**2/sigma2**2))/np.pi/sigma1/sigma2, axis=-1)

def potential_energy(position, psi, phi, sigma1, sigma2):
    # (A, )
    psi_, phi_ = position[:, 0], position[:, 1]
    # (A, R)
    delta_psi = psi_[:, None] - psi[None]
    delta_phi = phi_[:, None] - phi[None]
    # (A, )
    Z = -np.log(gaussian2(delta_psi, delta_phi, sigma1=sigma1, sigma2=sigma2, A=2.0)+1)
    return Z

data = np.genfromtxt('./cv_record.txt', delimiter=',')
phi = data[:, 0]
psi = data[:, 1]

num_grids = 100
num_levels = 10
psi_grids = np.linspace(-np.pi, np.pi, num_grids)
phi_grids = np.linspace(-np.pi, np.pi, num_grids)
grids = np.array(np.meshgrid(psi_grids, phi_grids)).T.reshape((-1, 2))

Z = potential_energy(grids, phi, psi, 1.0, 1.0).reshape((psi_grids.shape[0], phi_grids.shape[0])).T
X,Y = np.meshgrid(psi_grids, phi_grids)
levels = np.linspace(np.min(Z), np.max(Z), num_levels)

plt.figure()
plt.title("Biased MD Traj")
plt.xlabel(r'$\phi$')
plt.ylabel(r'$\psi$')
fc = plt.contourf(X, Y, Z, cmap='Greens', levels=levels)
plt.colorbar(fc)

plt.xlim(-np.pi, np.pi)
plt.ylim(-np.pi, np.pi)
plt.plot(phi, psi, 'o', alpha=0.4, color='red')
plt.savefig('meta.png')

总结概要

本文探索并梳理了一下CUDA SPONGE高性能分子模拟采样软件,和PySAGES高性能增强采样软件,这两者强强联合的MD模拟新范式。

版权声明

本文首发链接为:
https://www.cnblogs.com/dechinphy/p/pysages-sponge.html

作者ID:DechinPhy

更多原著文章:
https://www.cnblogs.com/dechinphy/

请博主喝咖啡:
https://www.cnblogs.com/dechinphy/gallery/image/379634.html

LinuxCNC是一款基于Linux操作系统的开源实时数控系统,可将普通计算机转变为高效的CNC(计算机数字控制)机器,本文记录xenomai下linuxcnc的构建简单记录,xenomai下构建无特别之处,主要参考链接
https://www.linuxcnc.org/docs/devel/html/code/building-linuxcnc.html

1.环境

软硬件环境

桌面环境:Ubuntu 24.04+xenomai3.3

硬件:rk3588(nanoPi R6/T6)

对xenomai内核要求

要在使linuxcnc在xenomai上运行,内核配置Local version必须是
-xenomai
,即
CONFIG_LOCALVERSION="-xenomai"
,这样linuxcnc编译配置时才能识别xenomai环境,否则不实时。

General setup  --->                                                                                             (-xenomai) Local version - append to kernel release

使用
uname -a
命令确认:

image-20241221223929726

对xenomai库要求

如果你需要编译生成linuxcnc debian安装包,由于生成linuxcnc debian安装包过程中会处理库的安装依赖生成依赖信息,这就要求xenomai库文件libcobalt.so属于某个debian包,通俗的说要求我们的xenomai库也是通过debian包安装的,而不是直接make install这种方式直接安装的,否则会产生如下类似错误。

dpkg-shlibdeps: error: no dependency information found for /usr/xenomai/lib/libcobalt.so.2 (used by debian/linuxcnc-uspace/usr/lib/libuspace-xenomai.so.0)

解决方式1:xenomai通过构建debian库来安装,可以参考本博客其他文章
编译构建xenomai库debian安装包

解决方式2:找到
Debian/rules
,打开之后找到
override_dh_shlibdeps
,在
dpkg-shlibdeps
那一行最后加上如下选项:

--dpkg-shlibdeps-params=--ignore-missing-info

以忽依赖信息。

安装依赖包

先安装依赖的工具和库,我遇到的有这些,不同的环境有差别,出错再对应安装不具备的即可。

 sudo apt install pkg-config build-essential  automake libtool m4 autoconf libudev-dev libmodbus-dev libusb-1.0-0-dev libgpiod-dev libglib2.0-dev libgtk-3-dev yapps2 intltool libboost-dev python3-dev libboost-python-dev  gtkwave bwidget tclx libeditreadline-dev python3-pip python3-tk libglu1-mesa-dev libxmu-dev asciidoc devscripts debhelper libtirpc-dev libtirpc-common tcl8.6-dev tk8.6-dev python3-opengl python3-full

2.本地编译linuxcnc

首先需要安装该工具(
sudo apt install git
)然后拉取代码,如下所示:

$ git clone https://github.com/LinuxCNC/linuxcnc.git linuxcnc-source-dir

配置

拉代码生成配置文件

$ git clone https://github.com/LinuxCNC/linuxcnc.git linuxcnc-source-dir
$ cd linuxcnc-source-dir/src
$ ./autogen.sh

生成得
configure
,它需要许多可选参数。通过运行以下命令列出
configure
的所有参数:

$ cd linuxcnc-source-dir/src
$ ./configure --help

最常用的参数是:

  • --with-realtime=uspace

    为任何实时平台或非实时平台构建。生成的 LinuxCNC 可执行文件将在带有 Preempt-RT 补丁的 Linux 内核(提供实时机器控制)和普通(未打补丁)Linux 内核(提供 G 代码模拟,但不提供实时机器控制)上运行。


    如果安装了 Xenomai(通常来自 libxenomai-dev 软件包)或 RTAI(通常来自名称以 “rtai-modules ”开头的软件包)的开发文件,也将启用对这些实时内核的支持。

  • --with-realtime=/usr/realtime-$VERSION

    使用旧的“内核实时”模型构建 RTAI 实时平台。这要求您在
    /usr/realtime-$VERSION
    中安装 RTAI 内核和 RTAI 模块。生成的 LinuxCNC 可执行文件将仅在指定的 RTAI 内核上运行。

  • --enable-build-documentation

    除了可执行文件之外,还构建文档。此选项会显着增加了编译所需的时间,因为构建文档非常耗时。如果不需要构建文档,则可省略此参数。

  • --disable-build-documentation-translation

    禁用为所有可用语言构建翻译文档。翻译文档的构建需要花费大量时间,因此如果不是真正需要的话,建议跳过它。

$ ./configure --with-realtime=uspace --enable-build-documentation
....
checking for xeno-config... /usr/bin/xeno-config
checking for realtime API(s) to use... uspace+xenomai
...
######################################################################
#                LinuxCNC - Enhanced Machine Controller              #
######################################################################
#                                                                    #
#   LinuxCNC is a software system for computer control of machine    #
#   tools such as milling machines. LinuxCNC is released under the   #
#   GPL.  Check out http://www.linuxcnc.org/ for more details.       #
#                                                                    #
#                                                                    #
#   It seems that ./configure completed successfully.                #
#   This means that RT is properly installed                         #
#   If things don't work check config.log for errors & warnings      #
#                                                                    #
#   Next compile by typing                                           #
#         make                                                       #
#         sudo make setuid                                           #
#          (if realtime behavior and hardware access are required)   #
#                                                                    #
#   Before running the software, set the environment:                #
#         . (top dir)/scripts/rip-environment                        #
#                                                                    #
#   To run the software type                                         #
#         linuxcnc                                                   #
#                                                                    #
######################################################################

配置编译

$ make -j $(nproc)
...
Linking rtapi_app
Linking libuspace-xenomai.so.0
....

之后,如果只想构建 LinuxCNC 的特定部分,可以在
make
命令行上命名想要构建的部分。例如,正在开发名为
froboz
的组件,则可以通过运行以下命令来构建其可执行文件:

$ cd linuxcnc-source-dir/src 
$ make ../bin/froboz

如果在支持实时的系统上运行(请参阅下面的
实时
部分),此时需要一个额外的构建步骤:

$ sudo make setuid

成功构建 LinuxCNC 后,就可以运行测试了:

$ source ../scripts/rip-environment
$ runtests

pi@NanoPi-R6S:~/linuxcnc-source-dir/src$ linuxcnc
LINUXCNC - 2.10.0~pre0
Machine configuration directory is '/home/pi/linuxcnc-source-dir/configs/sim/axis'
Machine configuration file is 'canterp.ini'
Starting LinuxCNC...
linuxcncsvr (61089) emcsvr: machine 'Canterp Example'  version '1.1'
linuxcnc TPMOD=tpmod HOMEMOD=homemod EMCMOT=motmod
Note: Using XENOMAI (posix-skin) realtime
milltask (61103) task: machine 'Canterp Example'  version '1.1'
halui (61105) halui: machine 'Canterp Example'  version '1.1'
Found file(LIB): /home/pi/linuxcnc-source-dir/lib/hallib/basic_sim.tcl
....

这也可能会失败!阅读整个文档,尤其是
设置测试环境
部分。

3. 构建debian安装包

构建 Debian 软件包时,LinuxCNC 从源代码编译并包含依赖信息,同时可选地包含文档(这会增加构建时间,但可以跳过)。编译后的 LinuxCNC 存储在
.deb
文件中,该文件可安装在相同架构的任何计算机上。安装后,LinuxCNC 可在
/usr/bin

/usr/lib
中运行,如同其他系统软件。

此构建模式主要用于:

  • 打包软件以交付给最终用户。
  • 为未安装构建环境或无法访问互联网的计算机构建软件。

构建 Debian 软件包需要
dpkg-buildpackage
工具(由
dpkg-dev
提供),并确保所有必要的脚本都已安装,这通常通过安装
build-essential
虚拟包来实现。

$ sudo apt-get install build-essential 

构建 Debian 软件包还要求安装所有特定于软件包的构建依赖项。安装所有构建依赖项的最直接方法是执行(从同一目录):

$ cd linuxcnc-source-dir
$ ./debian/configure
$ sudo apt-get build-dep .

可以使用
dpkg-checkbuilddeps
来检查依赖是否满足 (也来自作为构建必需依赖项的一部分安装的 dpkg-dev 软件包)程序来完成其工作(请注意,它需要从
linuxcnc-source-dir
目录运行:

$ dpkg-checkbuilddeps

满足这些先决条件后,构建 Debian 软件包包括两个步骤。

第一步是通过运行以下命令从 git 存储库生成 Debian 包脚本和元数据:

$ cd linuxcnc-dev
$ ./debian/configure

Note:
debian/configure
根据您构建的平台接受参数,

它默认在用户空间(“uspace”)中运行 LinuxCNC,期望 preempt_rt 内核将延迟降至最低。

  • no-docs
    :跳过构建文档。
  • uspace
    :配置为 Preempt-RT 实时或非实时(兼容两者),或使用
    noauto
    禁用自动检测。

配置 Debian 软件包脚本和元数据后,通过运行
dpkg-buildpackage
来构建软件包:

$ dpkg-buildpackage -b -uc -j$(nproc)

Note:

要构建的典型 Debian 软件包,您可以运行不带任何参数的 dpkg-buildpackage。如上所述,该命令传递了两个额外的选项。与所有优秀的 Linux 工具一样,手册页包含
man dpkg-buildpackage
的所有详细信息。

  • -uc

    不要对生成的二进制文件进行签名。仅当您想将软件包分发给其他人时,您才需要使用自己的 GPG 密钥对软件包进行签名。未设置该选项并且无法对包进行签名不会影响 .deb 文件。

  • -b

    这对于避免编译与硬件无关的内容非常有帮助,对于 LinuxCNC 来说就是文档。无论如何,该文档可以在线获取。

如果您在编译时遇到困难,请在线查看 LinuxCNC 论坛。目前正在出现的是对 DEB_BUILD_OPTIONS 环境变量的支持。将其设置为

  • nodocs

    要跳过构建文档,最好使用
    -B
    标志来 dpkg-buildpackage。

  • nocheck

    跳过 LinuxCNC 构建过程的自检。这可以节省一些时间并减少对某些可能不适用于您的系统的软件包(尤其是 xvfb)的需求。您不应该设置此选项来对构建按预期执行有额外的信心,除非遇到依赖项方面的困难。

环境变量可以与命令的执行一起设置,例如

DEB_BUILD_OPTIONS=nocheck dpkg-buildpackage -uc -B

4.安装构建的Debian软件包

Debian 软件包可以通过其 .deb 扩展名来识别。安装它的工具
dpkg
是每个 Debian 安装的一部分。
dpkg-buildpackage
创建的 .deb 文件可以在 linuxcnc-source-dir 上面的目录中找到,即在
..
中。要查看包中提供了哪些文件,请运行

dpkg -c ../linuxcnc-uspace*.deb

LinuxCNC 的版本将是文件名的一部分,旨在与星号匹配。列出的文件可能太多,无法显示在您的屏幕上。如果您无法在终端中向上滚动,请添加
| more
该命令的
| more
是使其输出通过所谓的“寻呼机”传递。用“q”退出。

要安装软件包,请运行

sudo dpkg -i ../linuxcnc*.deb

如果依赖不全,使用
sudo apt --fix-broken install
自动安装缺失的依赖。

安装完成后在ubuntu应用列表即可看到linuxcnc。

Screenshot from 2024-12-22 08-14-01

5. latency-test

通过命令行测试

pi@NanoPi-R6S:~$ latency-test
Note: Using XENOMAI (posix-skin) realtime

截图 2024-12-22 08-23-29

测试的是cpu0的实时性,会差一些,以隔离的cpu核latency测试为准。此时可以通过
cat /proc/xenomai/sched/threads
确认已经在xenomai内核调度运行。

pi@NanoPi-R6S:~$ cat /proc/xenomai/sched/threads
CPU  PID    CLASS  TYPE      PRI   TIMEOUT       STAT       NAME
  0  0      idle   core       -1   -             R          [ROOT/0]
  1  0      idle   core       -1   -             R          [ROOT/1]
  2  0      idle   core       -1   -             R          [ROOT/2]
  3  0      idle   core       -1   -             R          [ROOT/3]
  4  0      idle   core       -1   -             R          [ROOT/4]
  5  0      idle   core       -1   -             R          [ROOT/5]
  6  0      idle   core       -1   -             R          [ROOT/6]
  7  0      idle   core       -1   -             R          [ROOT/7]
  0  126087 rt     cobalt      0   -             X          rtapi_app
  0  126090 rt     cobalt     98   43us          D          rtapi_app
  0  126091 rt     cobalt     97   255us         D          rtapi_app
pi@NanoPi-R6S:~$ cat /proc/xenomai/sched/stat
CPU  PID    MSW        CSW        XSC        PF    STAT       %CPU  NAME
  0  0      0          2971573    0          0     00018008   83.6  [ROOT/0]
  1  0      0          0          0          0     00018000  100.0  [ROOT/1]
  2  0      0          0          0          0     00018000  100.0  [ROOT/2]
  3  0      0          0          0          0     00018000  100.0  [ROOT/3]
  4  0      0          0          0          0     00018000  100.0  [ROOT/4]
  5  0      0          0          0          0     00018000  100.0  [ROOT/5]
  6  0      0          0          0          0     00018000  100.0  [ROOT/6]
  7  0      0          0          0          0     00018000  100.0  [ROOT/7]
  0  126087 1          1          5          0     000680c0    0.0  rtapi_app
  0  126090 1          1759321    1759322    0     00048044   16.0  rtapi_app
  0  126091 1          43964      43964      0     00048044    0.4  rtapi_app

启动一个示例

截图 2024-12-22 08-23-47

linuxcnc2024-12-22 08-26-17

参考链接

linuxcnc官方文档

前言

我用chatGPT帮我写后端爬虫,分析知乎html代码,爬取知乎壁纸。然后用cursor AI工具,完全使我一个不懂前端uniapp框架的人,开发了一个小程序手机壁纸页面。

原来一周的工作量,半天搞定。
体验可以微信搜索《程序员博博》同名

配合chatGPT爬知乎

首先我们打开知乎首页,以《
有哪些你不舍得换的手机壁纸
》问题为例。发觉他是请求API获取问题的数据。

第一步
:我们首先使用chatGPT让他帮我们把这个API请求转化为我们的Java请求的代码,写出来非常优秀,可以直接使用。

第二步
:也是最关键的,我们把他爬取的html,让GPT进行正则分析。我们到底是需要问题的答案呢,还是需要问题的评论,还是需要问题里面的图片。GPT非常厉害,一下子就写出来,而且一把过。真的太厉害了。

使用cursor开发小程序

cursor开发前端真的厉害了,让不懂程序的人也可以开发一个纯前端页面。前提你必须设置一个好的规则给到cursor,让AI清楚明白你需要什么,他才可以准确无误的给出想要的答案,当然有时候要不停地问好几遍

第一步
:用cursor的compose创建一个全新的项目,具体看你的需求,比如我这次开发小程序,我就创建了一个全新的uniapp项目。

第二步
:创建完成项目。我一般会使用
chat+codebase功能
,来告诉curosr我需要什么东西,比如一个页面,比如交互,又比如弹窗样式等等。这时候你必须耐心,cursor很强大,但是你得准确无误告诉他你的需求,他才能发挥出他的功能。

最后一步:代码开发完成,你就可以上线代码了。你可以做一个网站,做一个小程序,甚至一个ios 安卓app都是可以的。我差不多只用了半天,一个晚上+一个上午就搞定了

最后

AI发展的太快了,快到你我都无法想象,以前一周的工作量,现在一天就干完了。你如果现在还不懂AI编程,相当于你10年前不知道如何使用智能机一样,注定会被时代所淘汰,时代淘汰你,连一声招呼都不会打。

建议大家都去使用下AI工具。尤其是写代码的小伙伴,不仅仅局限于chatGPT,还有cursor、WindSurf、v0、Bolt.new等等工具, 你会被惊艳到的。谢谢大家,大家如果还有什么想聊的,欢迎评论留言

.NET 9已经发布有一段时间了,近期整理一下.NET 9的新特性,今天重点分享.NET 9 JSON序列化方面的改进。

先引用官方的说明:

在 System.Text.Json 中,.NET 9 提供了用于序列化 JSON 的新选项和新的单一实例,可以更轻松地使用 Web 默认值进行序列化。

举个实际的例子,

缩进选项

JsonSerializerOptions
包括新的属性,可支持自定义写入 JSON 的缩进字符和缩进大小。

var options = new JsonSerializerOptions
{
    WriteIndented = true,
    IndentCharacter = '\t',
    IndentSize = 2,
};

string json = JsonSerializer.Serialize(
    new { Value = 1 },
    options
    );
Console.WriteLine(json);

实际的效果

//{
//                "Value": 1
//}

大家不禁会问,这有什么用?

JsonSerializerOptions
新增了对自定义 JSON 缩进字符和缩进大小的支持。这项特性主要通过两个新属性实现:

  1. IndentChars
    : 指定用于缩进的字符(默认为空格
    " "
    )。
  2. IndentSize
    : 指定缩进的大小(默认为 2)。

这些属性使开发者能够更灵活地控制生成的 JSON 格式,适应不同的需求,比如:

  • 日志格式优化
    : 某些系统对日志的可读性有特定要求,需要用不同的缩进字符(例如制表符
    \t
    )或更大的缩进层次,以更容易定位层次结构。

  • 前端需求匹配
    : 一些前端框架可能要求特定的 JSON 格式(例如 4 个空格的缩进),以便与前端代码风格保持一致。

  • 节省空间
    : 在生成用于传输或存储的美化 JSON 时,调整缩进字符或大小可以帮助减少文件体积。

  • 与外部工具集成
    : 某些外部工具或系统对 JSON 的缩进风格有特定要求,自定义这两项属性可以轻松实现兼容。

继续给一个示例代码

usingSystem;usingSystem.Text.Json;public classProgram
{
public static voidMain()
{
var data = new{
Name
= "Alice",
Age
= 30,
Skills
= new[] { "C#", "ASP.NET Core", "Blazor"}
};
//默认缩进格式 var defaultOptions = newJsonSerializerOptions
{
WriteIndented
= true};
Console.WriteLine(
"默认格式:");
Console.WriteLine(JsonSerializer.Serialize(data, defaultOptions));
//使用自定义缩进字符(制表符)和大小 var customOptions = newJsonSerializerOptions
{
WriteIndented
= true,
IndentChars
= "\t", //使用制表符 IndentSize = 1 //每层缩进 1 个制表符 };
Console.WriteLine(
"\n自定义制表符缩进:");
Console.WriteLine(JsonSerializer.Serialize(data, customOptions));
//使用自定义空格缩进 var customSpaceOptions = newJsonSerializerOptions
{
WriteIndented
= true,
IndentChars
= " ", //使用空格 IndentSize = 4 //每层缩进 4 个空格 };
Console.WriteLine(
"\n自定义空格缩进:");
Console.WriteLine(JsonSerializer.Serialize(data, customSpaceOptions));
}
}

输出数据示例

1. 默认格式

{"Name": "Alice","Age": 30,"Skills": ["C#","ASP.NET Core","Blazor"]
}

2.自定义制表符缩进

{"Name": "Alice","Age": 30,"Skills": ["C#","ASP.NET Core","Blazor"]
}

3. 自定义空格缩进

{"Name": "Alice","Age": 30,"Skills": ["C#","ASP.NET Core","Blazor"]
}

这种灵活性让开发者能根据不同的上下文需求生成适合的 JSON 输出,既方便了可读性,也提升了兼容性。

然后,我们继续看新的单一实例
如果要使用
ASP.NET Core 用于 Web 应用的默认选项
进行序列化,请使用新的
JsonSerializerOptions.Web
单一实例。
string webJson =JsonSerializer.Serialize(new { SomeValue = 42},
JsonSerializerOptions.Web
//Defaults to camelCase naming policy. );
Console.WriteLine(webJson);
//{"someValue":42}

其实就是给JsonSerializerOptions多了一个Web选项。官方的解释:

意会之后,整理了以下说明:

JsonSerializerOptions.Web
是一个新的单例实例,专为 Web 应用程序设计,提供了默认的 JSON 序列化选项。它的主要特点包括:

  • 属性命名策略
    :将属性名称从 PascalCase 转换为 camelCase,以符合 JavaScript 的命名惯例。

  • 忽略默认值
    :在序列化过程中,忽略属性的默认值(如
    null

    0

    false
    ),以减少传输的数据量。

  • 数字处理
    :允许从字符串读取数字,以提高灵活性。

这些默认设置使得在 Web 环境中进行 JSON 序列化时,无需手动配置即可获得符合 Web 标准的输出。

实际应用场景

  • Web API 开发
    :在构建 RESTful API 时,使用
    JsonSerializerOptions.Web
    可以确保返回的 JSON 数据符合前端的预期格式,减少前后端数据格式不一致的问题。

  • 前后端一致性
    :通过自动将属性名称转换为 camelCase,避免了前端在处理 JSON 数据时的额外转换工作,提高开发效率。

给个示例代码看看效果

usingSystem.Text.Json;public classProduct
{
public int Id { get; set; }public string Name { get; set; } = "Sample Product";public decimal Price { get; set; } = 0.0m; //默认值 public string? Description { get; set; } = null; //可空类型 }public classProgram
{
public static voidMain()
{
var product = newProduct();//使用 JsonSerializerOptions.Web 进行序列化 string json =JsonSerializer.Serialize(product, JsonSerializerOptions.Web);

Console.WriteLine(json);
}
}

输出的JSON数据

{"id": 0,"name": "Sample Product"}
  • 属性命名策略

    Id
    被序列化为
    id

    Name
    被序列化为
    name
    ,符合 camelCase 命名惯例。

  • 忽略默认值

    Price
    属性的默认值为
    0.0

    Description
    属性的默认值为
    null
    ,因此在序列化结果中被省略。

以上是对.NET 9中JSON序列化部分的研究、分享和介绍。

周国庆

2024/12/22