Flink近一月的开拓记录和沉淀分析

前言

我从6月到现在8月底中间两个月时间呢,除了日常项目之外,大部分时间都在搞数据同步那个项目,作为团队内部的小数仓,我个人在从零开始的路上也是积累了很多经验。网上我有看过尚硅谷的数仓项目,咋说呢,都是用flink-cdc去做,对我来说没有太多参考价值。之前我也有提到过,MySQL数据源是cdc做的,这没毛病,但是Oracle是另一套做法,详情见如何巧妙解决Flink数据倾斜问题。虽然Oracle的cdc官方也有支持,但是这个OGG是外采的,领导说不支持,那就只能硬着头皮转换SQL来做,就很难。

两个月之前的时间解决了准确的问题,这两个月着重解决速度的问题。主要是对Flink的一些特性进行了深入学习和实践,在8月30日这天算是有了阶段性的结果,由此沉淀下这一两月的知识。虽说这两月我没有需求开发任务,但是运维还是不少,占比有四分之一的时间,没法全身心投入,主要是老被紧急问题打断,这就很烦。现在回头来看这两月的重要结果,代码整体优化依赖升级(flink升到1.17.1,fastjson2最新,日志优化,切除冗余依赖),Sql处理升级为批量,Yarn集群提交从session替换为per-job,此外还有不少BUG解决。接下来我想通过时间线来一步步的描述我走的弯路和尝试,It's Showtime!

分流-侧输出流与键控流

一开始考虑用侧输出流,是因为他的功能很符合我的场景,从主流分出去支流,各干各的互不干扰。可以从主流中分出去多个支流,并且支流的数据类型不受主流限制。

Flink侧输出流版本

部署环境

flink 1.17.1,配置3台节点集群,单节点TM(taskmanager)设置为4Slot,总内存16G

代码逻辑

分流版本代码代码重新开发,其核心思路是利用了侧输出流,在不阻塞主流的情况下提升QPS。设计思路是按照之前keyBy的路径,尽量平衡每个支流的数据输入输出,由于旁路输出与主流共享一个Slot,因此减少了TM的Slot数提升单个Slot的内存大小以期望获得更好的性能。

实现效果

从上图看,无论是从TIDB的QPS面板还是Flink下面接收数据的红框数据都可以得到一个数字,300+。从flink看板的算子图可以看出,出现了严重背压,与预期不同的是支流繁忙导致背压严重会阻塞主流,从而使整体速度下降。

在此过程中可以观察到各项指标正常。flink的JM和TM的内存使用情况正常,GC看起来也正常,这里发现一个有意思的事情,在1.17.1默认使用的是新的G1垃圾回收器,而1.15.2使用的是JD8默认的PS,如果可以的话推荐升一下JDK版本。

在观察TIDB看板后,延迟200ms正常,因为只涉及到一个ptm_erp数据库,所以压力大部分在一个TIDB和TIKV上,142服务器的CPU使用率和内存使用率正常上升,均处于正常水平。

因为142机器上同时部署了Flink和TIDB,所以特意查看了内存占用,还算正常

CPU部分也算正常

Flink的键控流(KeyBy)版本

部署环境

flink 1.17.1,配置3台节点集群,单节点TM设置为16Slot,共计16G

代码逻辑

该版本为基于线上2.0版本微调,仅对transform算子部分的并行度增大,并扩充了两张大表,Sink并行度划分一共32个Slot。

实现效果

从上图来看,和分流版本差不多,但是仍然出现了严重的背压,主要集中在Sink端算子,太慢

TIDB部分和分流版本几乎一致

多个TM的占用上升,并且可观测到GC略微增加,判断可能是单个Slot资源过少的情况

flink多任务KeyBy模式

部署环境

flink 1.17.1,配置3台节点集群,单节点TM设置为16Slot,共计16G,和普通版本无区别。

代码逻辑

基于keyBy版本,按照重写后的Key算法将数据切分为两块,划分为两个服务,分别提交为独立flink任务

实现效果

非常惊喜和意外,QPS攀升到了五百多,相比之前的三百多,大约有1.5倍的提升,并且该服务数据并未完全平分。

从下图看,仍然存在着严重背压,因为16个并行度中仍然存在阻塞,两个服务的数据也能看出未能均分,一个服务已运行4.7G,而另一个服务只运行了1.4G,倘若能均分,QPS应该还能提升

各个TM表现正常,TIDB由于QPS上升,CPU使用率和内存占用量少量提升,均属于正常范围

分流方案开发测试总结

分流方案我这一共实验了三种,分别是侧输出流、键控流和拆分任务的键控流,本质上都是通过并发处理提速,结论是拆分任务最有效。先叠个甲,博主也是在学习中,如有不对快点告诉我吧,真不想走弯路了。一个月后回头来分析,当时我对侧输出流的期望出现了问题,我完全没有想过支流会反过来阻塞主流的问题,我天真的以为支流流出去了就不用管了,后续数据往上堆就行了。但从flink的设计角度思考,很难允许这样不受控制的数据流存在啊。

键控流是我在处理1.0版本顺序问题砥砺前行-初学Flink的我如何快速定位并解决数据同步问题时用到的优化版本,代码保持不变。拆分任务这个是怎么想出来的呢,主要是破除键控流版本的困境,如果有一张大表执行太慢,那么就会阻塞整个流,其他分支都不动了,就这个大表对应Key的Slot在老牛拉破车,一核有难九核围观。拆分任务的原理就是,把大表拎出来,不能因为这一个慢就影响别的Slot运行。经历以上开发测试后,我就继续对原本的大任务就行拆分,并做了数据倾斜的处理。

期间的困境与弯路

在以上分流方案实施后,虽然稳定了,但是仍然没有完全解决速度上的问题,原因是,就算SQL执行再快就几毫秒或者几十毫秒,那人家一条SQL修改上千上万条SQL,通过OGG传过来就是上千上万条数据,转换成SQL也是上千上万条。换算下来,人家一条语句需要我这边通过更多的语句去执行,就像是MySQL的BinLog,我肯定拍马也追不上人家数据源的更新速度。

虽说一直解决的是不同的问题,但是困难的是,数据同步的一切问题呈现的表象都是一样的,无论是慢了还是执行错了,对用户来说就是数据有问题。很无语,这段时间我也一直在想法提升速度,中间走了很多弯路,同时也解决了很多问题。

部署模式的升级失败

原始版本也就是1.0版本用的Flink是1.15.2,包括线上和测试环境部署的都是这个版本。测试环境部署的是flink集群模式,正式环境部署的是Yarn集群的Session(会话)模式。

测试环境用的flink集群无所谓,我替换1.17.1后无缝升级,具体配置我就不详述了。正式环境我想部署成per-job(单任务)或者application(应用)模式,为啥呢?因为Session模式资源共享。我就遇到一个问题,有一个任务阻塞了,导致这个任务对应的taskManager心跳超时,进而TM丢失,有用到这个TM上面的Slot的任务就全都挂了。单任务或者应用模式资源隔离,那就不会有这个问题,至少不会影响别的任务,这很关键。当时Hadoop和Yarn的配置有问题,导致flink的任务提交出现了问题,但是那会儿我没有意识到,单纯是以为我提交的命令和flink配置的不对,经过接近一天的尝试后,无奈放弃。

8.28追加更新--解决了这个问题,Hadoop的classpath没有配置,还有各个服务器的Hadoop环境变量并没有都配上,重启后目前以yarn-per-job模式已正常提交任务。

arduino 复制代码
./flink run -t yarn-per-job -d -Dyarn.application.name="erpPro6" -Djobmanager.memory.process.size=2g -Dtaskmanager.memory.process.size=8g -Dtaskmanager.numberOfTaskSlots=4  -Dyarn.containers.vcores=6 -c com.it.ruijie.basedata.app.CommonApp  job/flink-ogg3.jar -config ogg-erp-pro-6

以上配置的的意思如下,-t yarn-per-job表示以单任务模式提交,-d分离模式就是后台运行,-Dyarn.application.name="erpPro6"提交到Yarn集群的名字是erpPro6,-Djobmanager.memory.process.size=2g意思是jobmanager总内存大小为2G,-Dtaskmanager.memory.process.size=8g就是单个taskmanager总内存8G,-Dtaskmanager.numberOfTaskSlots=4意思是单个TM划分为4个Slot,-Dyarn.containers.vcores=6使用到Yarn容器的CPU要六核(这个需要改Yarn的配置,目前未生效)。后面就是常规配置了,-c入口类,指定jar包位置,-config ogg-erp-pro-6这个是我自定义的参数,不用管。

数据库断连

这个Communications Link Failure错误真的是传世经典,背后的原因多种多样。在看到这个错误的时候,我以为是SQL处理超时导致数据库连接失效,但是想了想不对,因为通过TIDB后台可以看到没有运行时间超过我设置的超时时间的SQL出现。然后一个奇异的共性现状引起了我的注意,从我提交任务到出现断连异常的时间差不多是8小时,8小时?这不是MySQL侧的默认连接保活时间吗,再一看我的代码,当时瞎逼优化,为了节省创建数据库连接的开销,选择在Sink的open方法中固化连接。

这里解决也很简单,回退到之前的代码,选择固化连接池即可。这里不由得感概我也算见多识广,这8小时的特殊情况,我一眼顶针。

优化大乱炖

代码优化

  1. 代码因为我是切分成多任务,为了避免出现多份类似的代码,我做了一些配置化的工作,通过打包时添加参数,来加载不同的配置。
  2. 此外优化了依赖,缩小包体积以及提高启动速度。
  3. 弃用fastjson1,改用fastjson2,提升序列化速度,代码中序列化次数频繁,因此该项很有必要,特别是应对大规模流量冲击的时候

DataX使用指南

之前全量数据同步的时候,额,让大家见笑了,用的Navicat数据传输。从Oracle传到TiDB,自动建表加传输是真的香,但也埋下了隐患。Navicat在TiDB建表时为了保证Oracle的数据能顺利传输,字段类型只要有模糊的值,统统拉到最大,比如小数类型,整数位和小数位直接拉满。造成的影响就是数据膨胀,我在重新优化表结构后,数据大小少说也能小个四分之一,多的能少一半。

在重新建表的时候呢,为了能快速的传输全量数据,引入了DataX。

快速部署

bash 复制代码
wget https://datax-opensource.oss-cn-hangzhou.aliyuncs.com/202303/datax.tar.gz  (链接来自GITHUB,右键复制链接,别下载了还往服务器传,浪费时间)
tar -zxvf datax.tar.gz 
检查JDK和Python是否安装
java -version   python
下载后解压至本地某个目录,进入bin目录,即可运行同步作业:
cd  {YOUR_DATAX_HOME}/bin
python datax.py {YOUR_JOB.json}
自检脚本:    python {YOUR_DATAX_HOME}/bin/datax.py {YOUR_DATAX_HOME}/job/job.json

使用指南

DataX官方文档,基本看文档就好,我看有个第三方写的定时平台,内嵌XXL-JOB来执行脚本,但是好多年不更新了,不知道还能不能用了。官方有提供付费升级服务,看着是挺爽,可惜我肯定是没法用了,没经费。分享一个我常用的脚本模板,其实配置很简单,看了官方的文档,其实也没啥可配置的,大部分都是限制而不是强化,默认就很强了。

lua 复制代码
{
  "job": {
    "setting": {
      "speed": {
        "channel": 16--通道数(并发数)。新版本DataX3.0提供了包括通道(并发)、记录流、字节流三种流控模式,一般推荐用通道控制。
      }
    },
    "content": [
      {
        "reader": {
          "name": "oraclereader",
          "parameter": {
            "username": "用户名",
            "password": "密码",
            "splitPk": "拆分主键",--推荐splitPk用户使用表主键,因为表主键通常情况下比较均匀,因此切分出来的分片也不容易出现数据热点
            "column": ["字段"],--尽量不要用*,插入有字段顺序的,不能保证原表结构和现表结构完全一致的别用*
            "connection": [
              {
                "table": [
                  "CUX.表名"
                ],
                "jdbcUrl": [
                  "jdbc:oracle:thin:@xxx:xxx"
                ]
              }
            ]
          }
        },
        "writer": {
          "name": "mysqlwriter",
          "parameter": {
            "batchSize": 3072, --批次和上面的channel联合测试,得到一个最佳效率点
            "writeMode": "insert",--最好是insert,这个快
            "username": "用户名",
            "password": "密码",
            "column": ["字段"],
            "preSql": ["TRUNCATE 输出表"],
            "connection": [
              {
                "jdbcUrl": "jdbc:mysql://ptm-rtc-tidb.ruijie.com.cn:4000/xxx?rewriteBatchedStatements=true&allowMultiQueries=true&useConfigs=maxPerformance&useServerPrepStmts=true&connectTimeout=300000&socketTimeout=600000&useSSL=false",
                "table": [
                  "输出表"
                ]
              }
            ]
          }
        }
      }
    ]
  }
}

操作要点

执行命令用下面这俩模板就好,一个是后台输出的,一个是指定了JVM堆内存大小。

ruby 复制代码
nohup python datax.py mtl.json > /usr/tool/datax/bin/mtl.log 2>&1 &
python /data/datax/bin/datax.py --jvm="-Xms8G -Xmx8G" /data/datax/job/erp-test/TEST-MTL_GENERIC_DISPOSITIONS_DATAX.json

指定堆内存大小的原因是,前面通道数和批次大小影响一次性读取的总数据大小,内存小了装不下会报OOM。还要注意的是,DataX读取的是快照数据,也就是传输这个时间段的数据是丢失的,需要额外补一下。总的来说,作为一个ETL工具,DataX足够便捷、稳定和快速,很好用。对了,提一嘴,jdbcurl后面那一串优化参数不用加,DataX默认会给你加上,这个不用担心。

SQL转换为批量操作

从八月上旬开始,着手进行批量的改造,我之前预估到了这是一个巨大的变动,因为这是整个底层处理逻辑的革新。原本的逻辑是对ROW模式的OGG日志进行数据行一对一的转换,这样就会出现天生的性能缺陷,删改的批量语句通过行记录转换,会出现一条SQL语句影响多行的情况。换言之,2.0的代码逻辑进行处理时,批量SQL通过OGG再到Flink进行转换后,会变成多个单行处理的SQL,想也知道成千上万条SQL怎么优化也赶不上一条SQL的执行速度。那么,如何巧妙地转换为批量SQL就是当下最重要的痛点。

问题现状

认识到这个问题的最直接现状就是一张频繁变动的小表(3W数据量),在ERP里这个表采用了很多批量操作。我单独拿下数据,做了SQL的转换,通过对数据的观察发现,短时峰值能达到上万QPS,平均下来每秒约一千QPS。前面我通过在线上问题的摸爬滚打中突击TIDB,观察TiDB的SQL时效,优化后大部分SQL维持在毫秒级,算上IO开销大部分也就几百毫秒,但是盖不住量大,因此长时间运算后,数据同步严重后置,往往还在处理几天前抛过来的数据。一开始我是选择单任务处理该表,不与其他表争夺资源后,独立运行应该会快些。但是可预料的是,依旧不行,因此要考虑批量处理。为了保证SQL的执行顺序,批量也不能随意批量。

方案需要考虑的问题

综合下来要考虑的有如下几个问题:

  1. 在批量时如何保证SQL执行的顺序
  2. 如何来做批量才更有效
  3. 批量多久提交,不能因为攒批而放弃实时

保证顺序

数据源是顺序的,需要保证转化成批量SQL同样保持顺序执行。我的解决方案是相同类型(增删改)的SQL进行攒批,遇到不同类型立刻转换之前的批数据成为SQL,发送到TIDB执行,最后将新的数据保留重新开始攒批。

更有效的批量

提到批量SQL,最简单的就是攒一批SQL语句一次性发送到TIDB执行,这样就减少了多次IO带来的开销,这是最轻松的。但是既然要做,那就优化到底。更新的语句,这没办法,只能攒批直接推。

新增的,攒批后统一改成批量增语句,类似于INSERT INTO表名VALUES ([列值],[列值])),([列值],[列值])),([列值],[列值]));。在做增语句时,要注意序列化时不要忽略NULL值数据,避免字段丢失,fastjson2的写法是JSON.toJSONString(obj, JSONWriter.Feature.WriteMapNullValue)。

删除的语句也好处理,攒批后按主键写IN查询删除。

保证时效

批量和时效通常是冲突的,强如Kafka内部也有批量大小和发送时延的博弈选择,因此我也需要提供这样的手段来保证数据的时效性。根据我对Flink的认知,初步设计了三套方案逐个实验,分别是Sink端加入自定义定时器、Flink定时器、时间窗口

我先选择的是使用自定义定时器,实现相对简单,最快出效果。但自定义定时器同样存在问题,来源于异步,基于flink对Sink的生命周期管理,开启异步的时候即使通过状态管理的全局变量进行控制,在极端情况下依旧存在数据丢失和数据覆盖的问题。

typescript 复制代码
public class TidbPowerSink extends RichSinkFunction<DataDTO> {
    private transient MapState<String, PendingData> pendingMap;
    private transient ValueState<Boolean> onTime;
    
     @Override
    public void open(Configuration parameters) {
        pendingMap = getRuntimeContext().getMapState(new MapStateDescriptor<>
                ("pendingMap", String.class, PendingData.class));
        onTime = getRuntimeContext().getState(new ValueStateDescriptor<>("onTime", Boolean.class, false));
        Executors.newScheduledThreadPool(1).scheduleAtFixedRate(() -> {
            try {
                invoke(new DataDTO<>(), new Context() {
                    @Override
                    public long currentProcessingTime() {
                        return 0;
                    }

                    @Override
                    public long currentWatermark() {
                        return 0;
                    }

                    @Override
                    public Long timestamp() {
                        return null;
                    }
                });
            } catch (Exception e) {
                log.error("定时任务出现异常", e);
            }
        }, 5, 5, TimeUnit.SECONDS);
    }
    @Override
    public void invoke(DataDTO dataRaw, Context context) {
        while (true) {
                Boolean value = false;
                try {
                    value = onTime.value();
                } catch (IOException e) {
                    log.error("忙循环重获取onTime失败", e);
                }
                if (!value) {
//                    log.info("从忙循环中脱离,继续流输出");
                    break;
                }
                try {
                    Thread.sleep(200); // 暂停200毫秒
                } catch (InterruptedException e) {
                    log.error("线程终端执行异常", e);
                }
            }
        ......

这里用到了Flink的状态管理,我的原意是将onTime作为判断当前流运行时是否同时有定时任务执行,就像上面invoke判断里的,如果当前定时任务执行,就阻塞当前流。不幸的是,我这种写法属于违规操作,在open里面开定时器调用invoke方法,我是真魔怔了,而且状态管理的字段不能应用于open中。测试该代码的时候,invoke里面的判断是失效的,并且定时器内部会疯狂报错,状态管理使用上的报错。归根结底就是invoke的调用是受到Flink的生命周期管理,我的做法是打破了规则,并且漏洞百出。

接下来测试的是flink官方的定时器。Flink的定时器通过源码得知会禁止数据的读取,优先执行定时器,但是缺点是不能在Sink端注入,因此原来的代码需要重新设计和大幅修改。

java 复制代码
 SingleOutputStreamOperator<DataDTO> process = filterStream.keyBy(new ReBalanceGroupSelector<>(parallelismSink))
        .process(new KeyedProcessFunction<Integer, DataDTO, DataDTO>() {
            @Override
            public void processElement(DataDTO dataDTO, KeyedProcessFunction<Integer, DataDTO, DataDTO>.Context context,
                                       Collector<DataDTO> collector) {
                TimerService timerService = context.timerService();
                //5秒后定时器
                timerService.registerProcessingTimeTimer(timerService.currentProcessingTime() + 5000L);
//                        collector.collect(dataDTO);
            }

            @Override
            public void onTimer(long timestamp, KeyedProcessFunction<Integer, DataDTO, DataDTO>.OnTimerContext ctx, Collector<DataDTO> out) throws Exception {
                super.onTimer(timestamp, ctx, out);
                //定时出发全量计算
                out.collect(new DataDTO(null, null, null, null, null, "all", null, ctx.getCurrentKey()));
                TimerService timerService = ctx.timerService();
                //5秒后定时器
                timerService.registerProcessingTimeTimer(timerService.currentProcessingTime() + 50000L);
            }
        });
process.keyBy(new ReBalanceGroupSelector<>(parallelismSink))
//                .addSink(new TidbPowerSink(parameterTool, tableMap))
        .print()

我对定时器的设想是,每隔五秒钟,将批次的数据处理后统一提交到Sink。但是实际测试后发现,是流中每一个数据传过来都会触发一次定时器,这肯定不对。那么我最开始的想法就是适合批处理而不是流处理,因此该方法废弃。

那么,就只剩下最后一招了,开窗。开窗的话,我选择的是十秒一次的固定时间窗口,然后用了一个Map存储各表的数据。这个做法类似于传统的批处理,缺点是时间点相对固定,会增大Sink端的TiDB的瞬时压力。

swift 复制代码
SingleOutputStreamOperator<List<DataDTO>> returns = filterStream
        .keyBy(new ReBalanceGroupSelector<>(parallelismSink))
        //使用系统处理时间,不用EventTime就无需设置水位线
        .window(TumblingProcessingTimeWindows.of(Time.seconds(10)))
        .process(new ProcessWindowFunction<DataDTO, List<DataDTO>, Integer, TimeWindow>() {
            @Override
            public void process(Integer integer, ProcessWindowFunction<DataDTO, List<DataDTO>, Integer, TimeWindow>.Context context,
                                Iterable<DataDTO> iterable, Collector<List<DataDTO>> collector) {
                List<DataDTO> list = new ArrayList<>();
                for (DataDTO dataDTO : iterable) {
                    list.add(dataDTO);
                }
                collector.collect(list);
                list.clear();
            }

        });
returns.addSink(new TidbPowerSink(parameterTool, tableMap))
        .setParallelism(parallelismSink).name(realJobName + "-sink");

目前基于时间窗口的方案,已测试通过,数据不会差太多,误差控制在3分钟内,相比之前大几天的延迟,和迟迟赶不上的尴尬情况已好了太多。3分钟的延迟更多来源于OGG本身采集和单通道的Kafka消费了几十张表数据过滤的时间。

批量的复盘

思路都比较直接,但代码其实改动非常大,一些小细节没法很好地展现出来。最后选择开窗,也是在时效和性能之间做出了妥协和平衡,我感觉这已经是批处理了,哈哈。总的开发时间其实有点赶,很多东西我都是半学半做,网上也缺乏完善的学习案例,算是磕磕绊绊地学习实践,不断碰壁的开拓之路。

当下遇到的问题

问题还是很多,简单罗列一下,避免自己忘记。

  1. Yarn集群的资源设置问题。我现在用yarn-per-job模式提交多个任务时,明明资源充足还是会提示我请求不到资源deployment took more than 60 seconds. Please check if the requested resources are available in the YARN cluster。
  2. yarn-per-job模式提交时,看不到flink控制台输出的日志。我用yarn-session模式就能看,本地也能看就很费解。
  3. checkPoints检查点机制没有打开,重试机制是缺失的,这块需要处理一下。

最主要的暂时就这俩问题,剩下的都是历史数据需要处理,重新建表,DataX推全量数据啥的。毕竟就我一个人搞,从设计到开发运维全包,太难了啊,时间不够。而且最困难的点在于学习资料很少,不像Java生态那么健全,很多都有解,flink相关的资料是真的少啊。

写在最后

今天周六加班,哎,真难啊,烦躁。借着周六加班的时间总结和沉淀一下,干了这么久也得好好回顾分析一下。想想都干了什么,有没有更进一步的地方,当时的考虑是不是周全,还有什么样的问题需要解决。哎,据说最近周末都要加班,心情很差,就这样吧。

相关推荐
一只栖枝2 小时前
华为 HCIE 大数据认证中 Linux 命令行的运用及价值
大数据·linux·运维·华为·华为认证·hcie·it
uzong3 小时前
技术故障复盘模版
后端
GetcharZp4 小时前
基于 Dify + 通义千问的多模态大模型 搭建发票识别 Agent
后端·llm·agent
桦说编程4 小时前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研4 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi4 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
阿华的代码王国5 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
Jimmy5 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
喂完待续6 小时前
Apache Hudi:数据湖的实时革命
大数据·数据仓库·分布式·架构·apache·数据库架构
AntBlack6 小时前
不当韭菜V1.1 :增强能力 ,辅助构建自己的交易规则
后端·python·pyqt