-
在信息技术飞速发展的今天,各种类型数据库层出不穷。由于支持数据在异构数据库间同步,逻辑复制的重要性与日俱增。当前openGauss逻辑复制串行解码平均性能为3~5MBps,在业务压力大的场景下难以满足实时同步的需求,导致日志堆积,从而影响生产集群业务。因此,我们设计了并行解码特性,令多个线程协同并行解码从而提高解码性能,在基础场景下解码性能可达到100MBps.设计思路——为什么考虑并行解码?原有的串行解码逻辑,从读取日志,到日志解码,以及结果拼接发送都是由单线程处理的,主要流程及耗时的大致比例如下图所示:可以看出,整个流程的主要时间消耗在解码步骤中,需要通过多线程解码来进行优化;而发送步骤的耗时也较为明显,可以使用批量发送来做进一步优化。工作流程——并行解码消息序列图如下所示,在并行解码中,openGauss的数据节点(Data Node, DN)上的工作线程被分为三种:Sender/Collector线程,负责接收客户的解码请求,以及拼接各解码线程的结果并发送给客户,该线程在一次解码请求中仅建立一个;Reader/Dispatcher线程,负责读取WAL日志并分发给各解码线程进行解码,该线程在一次解码请求中仅建立一个;Decoder线程,即解码线程,负责将Reader/Dispatcher线程发给自己的日志解码(当该线程已经在解码时,该部分日志暂存于read change队列中),并将解码结果(当尚未解到提交日志时,该部分结果暂存于decode change队列中)发给Sender/Collector线程,该线程在一次解码请求中可以建立多个。该消息序列图说明如下:客户端向DN发送逻辑复制请求,该DN可为主机或备机。逻辑复制选项里可以设置参数选择仅连接备机,以防止主机压力过大。除了接收到客户请求的Sender线程外,DN还需建立Reader/Dispatcher线程和若干Decoder线程。Reader读取xlog日志,进行预处理。如果相关日志涉及TOAST列,需在此步骤完成TOAST拼接。Dispatcher将预处理后的日志派发给各Decoder线程。Decoder线程各自独立进行解码。这里可以通过配置选项来设置解码格式(json、text或bin)。Decoder将解码结果发送给Collector。Collector以事务为单位汇总解码结果。为减少发送次数,降低网络I/O对解码性能的影响,在开启批量发送功能(即sending-batch参数设置为1)时,Sender积攒一定量的日志后(阈值设置为1MB),批量向客户返回解码结果。客户如需停止逻辑复制流程,断开与DN的逻辑复制连接即可。Sender向Reader/Dispatcher线程和Decoder线程发送退出信号。各线程收到退出信号后,释放占用的资源,完成环境清理并退出。技术细节1——可见性改造在逻辑解码中,由于是对历史日志进行解析,因此对日志中元组的可见性判断至关重要。在原有串行解码的逻辑里,我们使用活跃事务链表机制来进行可见性判断。但是对于并行解码,每个解码线程各自维护一个活跃事务链表代价较大,会对解码性能产生不利影响,因此我们做了可见性改造,使用CSN(Commit Sequence Number,即提交事务号)来进行元组可见性判断。针对每一个事务号xid,其可见性流程如下:其主要流程为:根据xid获取用来判断可见性的CSN,这里确保可以根据任一xid获取到CSN值。如果xid为异常值,会返回表示特定状态的CSN,这种CSN也可用于可见性判断;若CSN已提交,将其与快照中CSN比较,该事务CSN较小则不可见,否则可见;若CSN不是正在提交,则不可见。基于上述逻辑,在并行解码中,判断一个元组的快照可见性的逻辑则为依次判断元组Xmin(插入时的事务号)和Xmax(删除/更新时的事务号)的快照可见性。这里的整体思路是,Xmin不可见/未提交或Xmax可见则元组不可见,而Xmin可见且Xmax不可见/未提交则元组可见。元组中的各标记位维持其原有含义参与可见性判断。技术细节2——批量发送在使用并行解码之后,解码过程占用的时间出现显著下降,但此时解码结果发送线程又将成为瓶颈,对于每条解码结果均执行一次完整的发送流程代价太大。因此这里我们采用了批量发送的方式,将解码的结果暂时收集起来,在超过阈值时一并发送给客户端。在批量发送时,需额外记录每条解码结果的长度,以及约定好的分隔符,以便并行解码功能的使用者对批量发送的日志进行拆分处理。使用方式我们为并行解码新增了一些可选配置项:解码线程并发度通过配置选项parallel-decode-num,指定并行解码的Decoder线程数量。其取值范围为1~20的int型,取1表示按照原有的串行逻辑进行解码,不会进入本特性代码逻辑。默认值为1。当该选项配置为1时,禁止配置下面的解码格式选项decode-style。解码白名单通过配置选项white-table-list,指定需要解码的表。其取值为text类型包含白名单中表名的字符串,不同的表以’,'为分隔符进行隔离。例:select * from pg_logical_slot_peek_changes(‘slot1’, NULL, 4096, ‘white-table-list’, ‘public.t1,public.t2’);。限制仅备机解码通过配置选项standby-connection,指定是否限制仅备机解码。其取值为bool型,取true代表限制仅允许连接备机解码,连接主机解码时会报错退出;取false时不做限制。默认值为false。解码格式通过配置选项decode-style,指定解码格式。其取值为char型的字符’j’、’t’或’b’,分别代表json格式、text格式及二进制格式。默认值为’b’即二进制格式解码。批量发送通过配置选项sending-batch,指定是否批量发送解码结果。其取值为int型的0或1,0代表不开启批量发送,1代表解码结果累计达到或刚超过1MB时批量发送解码结果,这里默认值为0,即默认不开启批量发送。以使用JDBC进行并行解码为例,建立连接时需进行如下配置:PGReplicationStream stream = conn .getReplicationAPI() .replicationStream() .logical() .withSlotName(replSlotName) .withSlotOption("include-xids", true) .withSlotOption("skip-empty-xacts", true) .withSlotOption("parallel-decode-num", 10) .withSlotOption("white-table-list", "public.t1,public.t2") .withSlotOption("standby-connection", true) .withSlotOption("decode-style", "t") .withSlotOption("sending-bacth", 1) .start();从倒数第6行到倒数第2行为新增逻辑,代表10并发解码、仅解码表public.t1和public.t2、启用备机连接、解码格式为text且开启批量发送功能,在配置参数超出范围时,会报错并提示参数允许取值范围。辅助功能——监控函数在进行并行解码时,为在解码速度较慢的场景下方便定位解码性能瓶颈,我们增加了gs_get_parallel_decode_status()函数,用来查看当前DN上各解码线程的存储尚未解码日志的read change队列和存储尚未发送解码结果的decode change队列的长度。该函数没有入参,返回结果共四列,分别是slot_name、parallel_decode_num、read_change_queue_length、decode_change_queue_length。slot_name为复制槽名,类型为text;parallel_decode_num为并行解码线程数,类型为int;read_change_queue_length类型为text,其记录了每个解码线程的read change)队列长度;decode_change_queue_length类型为text,其记录了每个解码线程的decode change队列长度。使用方式如下所示:对于解码失速场景,在解码DN上执行此函数,然后查看查询结果的read_change_queue_length,这里记录每个解码线程里读取日志队列长度,如果这里的值过小表示日志读取阻塞,需继续定位是否为磁盘I/O不足。查看查询结果的decode_change_queue_length,这里记录每个解码线程里解码日志队列长度,如果这里值过小表示解码速度过慢,可以适当增加解码线程数量。如果read_change_queue_length和decode_change_queue_length均比较大,说明解码日志发送被阻塞,需检测并行解码使用者在目标数据库的回放日志速度。通常来说,解码失速场景一般有CPU、I/O、或内存资源不足造成,通过使用备机解码保证以上资源充足一般可以避免解码失速问题。结语并行解码可以大幅提升逻辑复制解码性能,其对解码实例增加的业务压力相较而言显得不那么重要。作为异构数据库数据复制的关键技术方案,并行解码对于openGauss的重要性也是不言而喻的。
-
# 一、问题现象 某个线下环境DN发生主备切换 # 二、问题影响 DN实例很频繁发生切换,业务出现断链情况 # 三、问题定位 ## 1.根据“由现象看问题本质”的原则,先看DN实例的日志 ``` source /opt/huawei/Bigdata/mppdb/.mppdbgs_profile cd $GAUSSLOG/pg_log/dn_6003 ll -thr vi postgres-xxx.log ``` 看到DN日志报:Too many open file in systems ## 2.查看OS日志 ``` cd /var/log tail -500f messages ``` 看到OS日志很多行报:VFS:file-max limit 640000 reached ## 3.进一步监控 ``` lsof -n|awk '{print $2}'|sort|uniq -c|sort -nr|more|head -n 20 ``` 本次处理打印了最多的20个文件句柄的进程,打印出结果的第二列为进程ID,可以观察是那个进程打开的文件过过多 ``` lsof -u omm |awk '{print $2}'| sort | uniq -c | sort -nr | head ``` 观察用户打开的文件句柄数 ``` ps -ef|grep 46905 ``` 也可以使用一下脚本监控: ``` #!/bin/bash while true do for PID in `lsof -u omm|awk '{print $2}'|sort|uniq -c|sort -nr| head | awk '{print $2}' | head -n 1` do date +'%Y-%m-%d %H:%M:%S' >> /home/omm/pid-2022-01-26.txt echo $PID >> /home/omm/pid-2022-01-26.txt lsof -p $PID >> /home/omm/pid-2022-01-26.txt sleep 1800 done done ``` ## 4.经确认文件句柄数达到阈值与近期新上的业务语句有关 好多表union all 和 join ## 5.调整omm用户文件句柄数 一般默认是1000000(100W),不建议改动,因为跟主机的综合性能有关,此处改为500W观察。
-
1、关于鲲鹏 1.1、鲲鹏介绍 鲲鹏计算产业是基于鲲鹏处理器的基础软硬件设施、行业应用及服务,涵盖从底层硬件、基础软件到上层行业应用的全产业链条。华为作为鲲鹏计算产业的成员,聚焦计算架构创新、处理器和开源基础软件的研发,以及华为云服务,致力于推动鲲鹏生态发展。通过战略性、长周期的研发投入,吸纳全球计算产业的优秀人才和先进技术,持续推进全栈计算技术的创新发展,加快构筑面向多样性计算的全球开源体系与产业标准。基于“硬件开放、软件开源、使能伙伴、发展人才”的策略推动鲲鹏计算产业发展。 1.2、鲲鹏解决方案 鲲鹏全栈解决方案,主要应用在金融、互联网、运营商、政府、电力、交通等行业。其中应用使能套件BoostKit可应用于大数据、分布式存储、数据库、虚拟化ARM原生等方面。基础软件可应用于openGauss企业级开源数据库、openEuler开源操作系统。开发套件DevKit包含鲲鹏代码迁移工具、鲲鹏编译器、鲲鹏性能分析工具、动态二进制翻译工具。 2、鲲鹏开发套件DevKit 2.1、DevKit介绍 鲲鹏开发套件DevKit提供涵盖代码开发、编译调试、云测服务、性能分析及系统诊断等各环节的开发使能工具,方便开发者快速开发出鲲鹏亲和的高性能软件,帮助开发者加速应用迁移和算力升级。同时面向全研发作业流程,提升应用迁移和调优效率,加速原生开发。 鲲鹏开发套件DevKit以开发者为中心,并提升全流程开发效率。 开发套件DevKit包含鲲鹏代码迁移工具、鲲鹏编译器、性能分析工具、动态二进制翻译工具等。 2.2、鲲鹏代码迁移工具 2.2.1、工具简介 鲲鹏代码迁移工具是一款可以简化客户应用迁移到基于鲲鹏916/920的服务器的过程的工具。工具仅支持x86 Linux到Kunpeng Linux的扫描与分析,不支持Windows软件代码的扫描、分析与迁移。 当客户有x86平台上源代码的软件要迁移到基于鲲鹏916/920的服务器上时,既可以使用该工具分析可迁移性和迁移投入,也可以使用该工具自动分析出需修改的代码内容,并指导用户如何修改。 鲲鹏代码迁移工具既解决了客户软件迁移评估分析过程中人工分析投入大、准确率低、整体效率低下的痛点,通过该工具能够自动分析并输出指导报告;也解决了用户代码兼容性人工排查困难、迁移经验欠缺、反复依赖编译调错定位等痛点。 2.2.2、应用场景 软件迁移评估:自动扫描并分析软件包(非源码包)、已安装的软件,提供可迁移性评估报告。 源码迁移:当用户有软件要迁移到基于鲲鹏916/920的服务器上时,可先用该工具分析源码并得到迁移修改建议。 软件包重构:帮助用户重构适用于鲲鹏平台的软件安装包。 专项软件迁移:使用华为提供的软件迁移模板修改、编译并产生指定软件版本的安装包,该软件包适用于鲲鹏平台。 增强功能:支持x86和鲲鹏平台GCC 4.8.5~GCC 9.3.0版本32位应用向64位应用迁移的64位运行模式检查,结构体字节对齐检查、缓存行对齐检查和鲲鹏平台上的内存一致性检查。 2.2.3、部署方式 单机部署,即将鲲鹏代码迁移工具部署在用户的开发、测试的x86服务器或者基于鲲鹏916/920的服务器。 2.3、鲲鹏性能分析工具 2.3.1、工具简介 鲲鹏性能分析工具由四个子工具组成,分别为:系统性能分析、Java性能分析、系统诊断和调优助手。 系统性能分析是针对基于鲲鹏的服务器的性能分析工具,能收集服务器的处理器硬件、操作系统、进程/线程、函数等各层次的性能数据,分析系统性能指标,定位到系统瓶颈点及热点函数,并给出优化建议。该工具可以辅助用户快速定位和处理软件性能问题。 Java性能分析是针对基于鲲鹏的服务器上运行的Java程序的性能分析和优化工具,能图形化显示Java程序的堆、线程、锁、垃圾回收等信息,收集热点函数、定位程序瓶颈点,帮助用户采取针对性优化。 系统诊断是针对基于鲲鹏的服务器的性能分析工具,提供内存泄漏诊断(包括内存未释放和异常释放)、内存越界诊断、内存消耗信息分析展示、OOM诊断能力、网络丢包等,帮助用户识别出源代码中内存使用的问题点,提升程序的可靠性,工具还支持压测系统,如:网络IO诊断,评估系统最大性能。 调优助手是针对基于鲲鹏的服务器的调优工具,能系统化组织性能指标,引导用户分析性能瓶颈,实现快速调优。 2.3.2、应用场景 客户软件在基于鲲鹏的服务器上运行遇到性能问题时,可用系统性能分析来快速分析和定位。 系统性能分析工具将采集系统如下数据: 系统软硬件配置和运行信息,例如:CPU类型、内存部署槽位、Kernel版本、内核参数、文件系统、系统运行日志参数等。 系统的CPU、内存、存储IO、磁盘IO等性能指标。 处理器PMU、SPE的性能数据。 处理器访问Cache/内存的次数、带宽、吞吐率等。 系统内核进行CPU资源调度、IO操作等数据。 进程/线程的CPU、内存、存储IO、上下文切换、系统调用等数据;进程命令行信息,包括:进程名、进程参数。 系统的热点函数及其调用栈;热点函数归属的程序/动态库(包含绝对路径);热点函数的汇编指令和热点指令;热点函数所对应的源代码(需要用户自行提供)。 2.3.3、部署方式 当前版本支持灵活部署,即将系统性能分析所有组件部署在一台服务器上、不同服务器上及混合部署,完成性能数据采集和分析。 2.4、鲲鹏开发套件插件工具(VSCode) 2.4.1、工具简介 鲲鹏开发套件插件工具是基于Visual Studio Code提供给开发者面向鲲鹏平台进行应用软件开发、迁移、编译调试、性能调优等一系列端到端工具,即插即用。一体化呈现代码迁移插件、鲲鹏开发框架插件、编译插件及性能分析插件的完整开发套件。 鲲鹏开发套件插件工具是一个工具集,由多个插件组成,支持IDE前端界面,支持一键式安装后端,代码编辑体验增强,自动检测安装鲲鹏编译器,编译调试,用例可视化,编码辅助,工程分析扫描。用户可以通过安装Kunpeng DevKit插件直接将四个插件都安装好,也可以单独选择个别插件安装使用。 2.4.2、代码迁移插件 鲲鹏代码迁移插件作为客户端调用服务端的功能,完成扫描迁移任务,可以对待迁移软件进行快速扫描分析,并提供专业的代码迁移指导,极大简化客户应用迁移到鲲鹏平台的过程。当客户有软件需要迁移到鲲鹏平台上时,可先用该工具分析可迁移性和迁移投入,以解决客户软件迁移评估中分析投入大、准确率低、整体效率低下的痛点。 代码迁移工具支持五个功能特性: 软件迁移评估:自动扫描并分析软件包(非源码包)、已安装的软件,提供可迁移性评估报告。 源码迁移:能够自动检查并分析出用户源码、C/C++/ASM/Fortran/解释型语言/汇编软件构建工程文件、C/C++/ASM/Fortran/解释型语言/汇编软件构建工程文件使用的链接库、x86汇编代码中需要修改的内容,并给出修改指导,以解决用户代码兼容性排查困难、迁移经验欠缺、反复依赖编译调错定位等痛点。 软件包重构:通过分析x86平台软件包(RPM格式、DEB格式)的软件构成关系及硬件依赖性,重构适用于鲲鹏平台的软件包。 专项软件迁移:基于鲲鹏解决方案的软件迁移模板,进行自动化迁移修改、编译、构建软件包,帮助用户快速迁移软件。 2.4.3、性能分析插件 鲲鹏开发套件是Visual Studio Code的一款扩展工具,通常将此类工具称作集成开发环境(IDE)插件。 鲲鹏性能分析插件是其中一个子工具,作为客户端调用服务端的功能。 鲲鹏性能分析工具由四个子工具组成,分别为:系统性能分析、Java性能分析、系统诊断和调优助手。 系统性能分析是针对基于鲲鹏的服务器的性能分析工具,能收集服务器的处理器硬件、操作系统、进程/线程、函数等各层次的性能数据,分析系统性能指标,定位到系统瓶颈点及热点函数,并给出优化建议。该工具可以辅助用户快速定位和处理软件性能问题。 Java性能分析是针对基于鲲鹏的服务器上运行的Java程序的性能分析和优化工具,能图形化显示Java程序的堆、线程、锁、垃圾回收等信息,收集热点函数、定位程序瓶颈点,帮助用户采取针对性优化。 系统诊断是针对基于鲲鹏的服务器的性能分析工具,提供内存泄漏诊断(包括内存未释放和异常释放)、内存越界诊断、内存消耗信息分析展示、OOM诊断能力,帮助用户识别出源代码中内存使用的问题点,提升程序的可靠性;压测网络,获得网络最大能力,为网络IO性能优化提供基础参考数据;诊断网络,定位网络疑难问题,解决因网络配置和异常而导致的网络IO性能问题;压测存储IO,获得存储设备最大能力,包括:吞吐量、IOPS、时延等,并以此评估存储能力,为存储IO性能优化提供基础参考数据。 2.5、二进制动态翻译工具 2.5.1、相关概念 ExaGear是一款二进制指令动态翻译软件,运行在ARM64服务器上,通过将x86的指令在运行时翻译为ARM64指令并执行,使得绝大部分Linux on x86应用无需重新编译就可运行在ARM64服务器上,实现低成本、快速迁移Linux on x86应用到ARM64服务器。 2.5.2、关键特性 支持多种部署方式:支持在物理机、虚拟机、容器等平台上部署; 部署简单:一键式快速安装,x86应用部署和运行与迁移前保持一致; 支持多版本Linux OS:目前支持CentOS 7、CentOS 8、Ubuntu18、Ubuntu20、OpenEuler 20.03,并且根据用户需求,未来可定制支持更多Linux OS发行; 低损耗: 大多数场景的应用,翻译损耗在20%以内。 3、结束语 对鲲鹏开发套件有兴趣的同学可参考如下链接进行进一步学习。 相关链接:https://support.huaweicloud.com/kunpengdevps/kunpengdevps.html
-
问题现象:分段扩容,重分步阶段UDF业务报错问题版本:HC DWS 8.1.1.202问题原因:udf相关作业中部分表未重分布,部分表已完成重分布,此种场景会跨nodegroup,会产生额外的stream线程,导致线程数过多,作业中涉及udf函数的线程增加最200左右的udf线程,从而业务报错。规避方法:手动将报错中的表、视图中的表手动提前进行重分布,在手动拉起重分步1.获取业务涉及的需要重分步的表比如获取视图中的表explain (stats on) 语句 可以获取到作业中所有的表cat xxxx |grep -i 'create table'|sort |uniqselect oid::regclass from pg_class where relname in (xxxx,xxxx);找到对应的表 按如下格式存成文本(没有列头)库名 schema 表名vmall_oar vmall_oar_cs behavior_content_liveactivityvmall_oar dwrdim dwr_dim_sku_dvmall_oar dwrdim dwr_dim_sys_ssid_cfgvmall_oar vmall_oc_0 ord_activity2.手动执行重分布gs_redis -p 8000 -d postgres -j 7 -r -m read-only -P /home/Ruby/aaa.txt3.待这些表重分布完成后,手动调起重分布nohup gs_expand -t redistribute --fast-redis --parallel-jobs=7 --redis-mode=insert &
-
上一篇:【Go实现】实践GoF的23种设计模式:SOLID原则简单的分布式应用系统(示例代码工程):https://github.com/ruanrunxue/Practice-Design-Pattern–Go-Implementation简述GoF 对单例模式(Singleton)的定义如下:Ensure a class only has one instance, and provide a global point of access to it.也即,保证一个类只有一个实例,并且为它提供一个全局访问点。在程序设计中,有些对象通常只需要一个共享的实例,比如线程池、全局缓存、对象池等。实现共享实例最简单直接的方式就是全局变量。但是,使用全局变量会带来一些问题,比如:客户端程序可以创建同类实例,从而无法保证在整系统上只有一个共享实例。难以控制对象的访问,比如想增加一个“访问次数统计”的功能就很难,可扩展性较低。把实现细节暴露给客户端程序,加深了耦合,容易产生霰弹式修改。对这种全局唯一的场景,更好的是使用单例模式去实现。单例模式能够限制客户端程序创建同类实例,并且可以在全局访问点上扩展或修改功能,而不影响客户端程序。但是,并非所有的全局唯一都适用单例模式。比如下面这种场景:考虑需要统计一个API调用的情况,有两个指标,成功调用次数和失败调用次数。这两个指标都是全局唯一的,所以有人可能会将其建模成两个单例SuccessApiMetric和FailApiMetric。按照这个思路,随着指标数量的增多,你会发现代码里类的定义会越来越多,也越来越臃肿。这也是单例模式最常见的误用场景,更好的方法是将两个指标设计成一个对象ApiMetric下的两个实例ApiMetic success和ApiMetic fail。那么,如何判断一个对象是否应该被建模成单例?通常,被建模成单例的对象都有“中心点”的含义,比如线程池就是管理所有线程的中心。所以,在判断一个对象是否适合单例模式时,先思考下,是一个中心点吗?UML结构代码实现根据单例模式的定义,实现的关键点有两个:限制调用者直接实例化该对象;为该对象的单例提供一个全局唯一的访问方法。对于 C++ / Java 而言,只需把对象的构造函数设计成私有的,并提供一个 static 方法去访问该对象的唯一实例即可。但 Go 语言并没有构造函数的概念,也没有 static 方法,所以需要另寻出路。我们可以利用 Go 语言 package 的访问规则来实现,将单例对象设计成首字母小写,这样就能限定它的访问范围只在当前package下,模拟了 C++ / Java 的私有构造函数;然后,在当前 package 下实现一个首字母大写的访问函数,也就相当于 static 方法的作用了。示例在简单的分布式应用系统(示例代码工程)中,我们定义了一个网络模块 network,模拟实现了网络报文转发功能。network 的设计也很简单,通过一个哈希表维持了 Endpoint 到 Socket 的映射,报文转发时,通过 Endpoint 寻址到 Socket,再调用 Socket 的 Receive 方法完成转发。因为整系统只需一个 network 对象,而且它在领域模型中具有中心点的语义,所以我们很自然地使用单例模式来实现它。单例模式大致可以分成两类,“饿汉模式”和“懒汉模式”。前者是在系统初始化期间就完成了单例对象的实例化;后者则是在调用时才进行延迟实例化,从而一定程度上节省了内存。“饿汉模式”实现// demo/network/network.go package network // 1、设计为小写字母开头,表示只在network包内可见,限制客户端程序的实例化 type network struct { sockets sync.Mapvar instancevar instance } // 2、定义一个包内可见的实例对象,也即单例 var instance = &network{sockets: sync.Map{}} // 3、定义一个全局可见的唯一访问方法 func Instance() *network { return instance } func (n *network) Listen(endpoint Endpoint, socket Socket) error { if _, ok := n.sockets.Load(endpoint); ok { return ErrEndpointAlreadyListened } n.sockets.Store(endpoint, socket) return nil } func (n *network) Send(packet *Packet) error { record, rOk := n.sockets.Load(packet.Dest()) socket, sOk := record.(Socket) if !rOk || !sOk { return ErrConnectionRefuse } go socket.Receive(packet) return nil }那么,客户端就可以通过 network.Instance() 引用该单例了:// demo/sidecar/flowctrl_sidecar.go package sidecar type FlowCtrlSidecar struct {...} // 通过 network.Instance() 直接引用单例 func (f *FlowCtrlSidecar) Listen(endpoint network.Endpoint) error { return network.Instance().Listen(endpoint, f) } ...“懒汉模式”实现众所周知,“懒汉模式”会带来线程安全问题,可以通过普通加锁,或者更高效的双重检验加锁来优化。不管是哪种方法,都是为了保证单例只会被初始化一次。type network struct {...} // 单例 var instance *network // 定义互斥锁 var mutex = sync.Mutex{} // 普通加锁,缺点是每次调用 Instance() 都需要加锁 func Instance() *network { mutex.Lock() if instance == nil { instance = &network{sockets: sync.Map{}} } mutex.Unlock() return instance } // 双重检验后加锁,实例化后无需加锁 func Instance() *network { if instance == nil { mutex.Lock() if instance == nil { instance = &network{sockets: sync.Map{}} } mutex.Unlock() } return instance }对于“懒汉模式”,Go 语言还有一个更优雅的实现方式,那就是利用 sync.Once。它有一个 Do 方法,方法声明为 func (o *Once) Do(f func()),其中入参是 func() 的方法类型,Go 会保证该方法仅会被调用一次。利用这个特性,我们就能够实现单例只被初始化一次了。type network struct {...} // 单例 var instance *network // 定义 once 对象 var once = sync.Once{} // 通过once对象确保instance只被初始化一次 func Instance() *network { once.Do(func() { // 只会被调用一次 instance = &network{sockets: sync.Map{}} }) return instance }扩展提供多个实例虽然单例模式从定义上表示每个对象只能有一个实例,但是我们不应该被该定义限制住,还得从模式本身的动机来去理解它。单例模式的一大动机是限制客户端程序对对象进行实例化,至于实例有多少个其实并不重要,根据具体场景来进行建模、设计即可。比如在前面的 network 模块中,现在新增一个这样的需求,将网络拆分为互联网和局域网。那么,我们可以这么设计:type network struct {...} // 定义互联网单例 var inetInstance = &network{sockets: sync.Map{}} // 定义局域网单例 var lanInstance = &network{sockets: sync.Map{}} // 定义互联网全局可见的唯一访问方法 func Internet() *network { return inetInstance } // 定义局域网全局可见的唯一访问方法 func Lan() *network { return lanInstance }虽然上述例子中,network 结构有两个实例,但是本质上还是单例模式,因为它做到了限制客户端实例化,以及为每个单例提供了全局唯一的访问方法。提供多种实现单例模式也可以实现多态,如果你预测该单例未来可能会扩展,那么就可以将它设计成抽象的接口,让客户端依赖抽象,这样,未来扩展时就无需改动客户端程序了。比如,我们可以 network 设计为一个抽象接口:// network 抽象接口 type network interface { Listen(endpoint Endpoint, socket Socket) error Send(packet *Packet) error } // network 的实现1 type networkImpl1 struct { sockets sync.Map } func (n *networkImpl1) Listen(endpoint Endpoint, socket Socket) error {...} func (n *networkImpl1) Send(packet *Packet) error {...} // networkImpl1 实现的单例 var instance = &networkImpl1{sockets: sync.Map{}} // 定义全局可见的唯一访问方法,注意返回值时network抽象接口! func Instance() network { return instance } // 客户端使用示例 func client() { packet := network.NewPacket(srcEndpoint, destEndpoint, payload) network.Instance().Send(packet) }如果未来需要新增一种 networkImpl2 实现,那么我们只需修改 instance 的初始化逻辑即可,客户端程序无需改动:// 新增network 的实现2 type networkImpl2 struct {...} func (n *networkImpl2) Listen(endpoint Endpoint, socket Socket) error {...} func (n *networkImpl2) Send(packet *Packet) error {...} // 将单例 instance 修改为 networkImpl2 实现 var instance = &networkImpl2{...} // 单例全局访问方法无需改动 func Instance() network { return instance } // 客户端使用也无需改动 func client() { packet := network.NewPacket(srcEndpoint, destEndpoint, payload) network.Instance().Send(packet) }有时候,我们还可能需要通过读取配置来决定使用哪种单例实现,那么,我们可以通过 map 来维护所有的实现,然后根据具体配置来选取对应的实现:// network 抽象接口 type network interface { Listen(endpoint Endpoint, socket Socket) error Send(packet *Packet) error } // network 具体实现 type networkImpl1 struct {...} type networkImpl2 struct {...} type networkImpl3 struct {...} type networkImpl4 struct {...} // 单例 map var instances = make(map[string]network) // 初始化所有的单例 func init() { instances["impl1"] = &networkImpl1{...} instances["impl2"] = &networkImpl2{...} instances["impl3"] = &networkImpl3{...} instances["impl4"] = &networkImpl4{...} } // 全局单例访问方法,通过读取配置决定使用哪种实现 func Instance() network { impl := readConf() instance, ok := instances[impl] if !ok { panic("instance not found") } return instance }典型应用场景日志。每个服务通常都会需要一个全局的日志对象来记录本服务产生的日志。全局配置。对于一些全局的配置,可以通过定义一个单例来供客户端使用。唯一序列号生成。唯一序列号生成必然要求整系统只能有一个生成实例,非常合适使用单例模式。线程池、对象池、连接池等。xxx池的本质就是共享,也是单例模式的常见场景。全局缓存…优缺点优点在合适的场景,使用单例模式有如下的优点:整系统只有一个或几个实例,有效节省了内存和对象创建的开销。通过全局访问点,可以方便地扩展功能,比如新增加访问次数的统计。对客户端隐藏实现细节,可避免霰弹式修改。缺点虽然单例模式相比全局变量有诸多的优点,但它本质上还是一个“全局变量”,还是避免不了全局变量的一些缺点:函数调用的隐式耦合。通常我们都期望从函数的声明中就能知道该函数做了什么、依赖了什么、返回了什么。使用使用单例模式就意味着,无需通过函数传参,就能够在函数中使用该实例。也即将依赖/耦合隐式化了,不利于更好地理解代码。对测试不友好。通常对一个方法/函数进行测试,我们并不需要知道它的具体实现。但如果方法/函数中有使用单例对象,我们就不得不考虑单例状态的变化了,也即需要考虑方法/函数的具体实现了。并发问题。共享就意味着可能存在并发问题,我们不仅需要在初始化阶段考虑并发问题,在初始化后更是要时刻注意。因此,在高并发的场景,单例模式也可能存在锁冲突问题。单例模式虽然简单易用,但也是最容易被滥用的设计模式。它并不是“银弹”,在实际使用时,还需根据具体的业务场景谨慎使用。与其他模式的关联工厂方法模式、抽象工厂模式很多时候都会以单例模式来实现,因为工厂类通常是无状态的,而且全局只需一个实例即可,能够有效避免对象的频繁创建和销毁。
-
需要使用C++的thread库,请问可以在CMakeLists中加-pthread吗
-
【功能模块】python写多进程处理时,利用 multiprocessing 中的 Manager 定义全局变量会导致线上报错:程序运行失败。【操作步骤&问题现象】1、线上提交如下代码:from multiprocessing import ManagerOUTPUT_PATH = "/output/solution.txt"if __name__ == '__main__': a = Manager().dict() with open(OUTPUT_PATH, "w") as f: pass会显示程序运行失败。2、但是将定义语句去掉,即:from multiprocessing import ManagerOUTPUT_PATH = "/output/solution.txt"if __name__ == '__main__': # a = Manager().dict() with open(OUTPUT_PATH, "w") as f: pass则不会显示程序运行失败,而是显示输出格式错误。【问题描述】上述操作都是在python自带的包中进行的。这说明在利用Manager定义多进程全局变量的时候会导致线上环境报错,而在线下是不会出现报错问题的。请问线上环境是出现这种报错是什么原因呢?
-
可能原因:安装过SysChecker未卸载干净,端口被占用1.检查SysChecker安装是否OMS主节点2.查看相关日志/opt/SysChecker/Output/SysCheckerRestLog.txt,发现报错信息Address already used3.检查端口24443是否被占用,4.netstat -ano|grep 24443发现24443端口被占用,SysChecker未卸载干净,有进程残留5.卸载Syschecker后,重新安装Syschecker对接环境 这个基本上是Syschecker出问题 由于现场不能使用root直接登录,所以在填入对接信息的时候不需要写入root密码,但是需要手动在主OMS节点安装Syschecker 重新安装后还是相同的问题,需要查看相关日志/opt/SysChecker/Output/SysCheckerRestLog.txt 发现报错信息 Address already used 判断可能是端口被占用了 netstat -anp| grep 24443 确认端口被占用。 杀掉占用端口的进程后,重新启动或重启安装Syschecker后问题解决。
-
@TOC五种IO模型阻塞IO阻塞IO: 在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式例如:A在钓鱼时,会一直盯着鱼漂,当鱼漂动时,拉动鱼竿,其余的时间都在盯着鱼漂。非阻塞IO非阻塞IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一般只有特定场景下才使用。例如:B也来钓鱼,B一遍玩手机一遍看着鱼漂,还时不时的问A钓没钓到鱼。B不是一直在等还可以玩手机的等。信号驱动信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作例:此时C也来钓鱼,C将鱼竿上系了一个铃铛,C在看书,当铃铛响了C开始拉动鱼竿。C就像是信号驱动IO一样。多路转接IO多路转接: 虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态例:此时D拿着20根鱼竿来钓鱼,D来回看着就可以了。20根鱼竿鱼上钩的几率远大于之前的3个人。异步IO异步IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).例:E带着手下来钓鱼,E说:你在这钓,把桶钓满了拿回来给我。E然后开车走了。E根本不用在这里等待。总结:任何IO都包括2个步骤:1是等,2是拷贝读IO=等待读事件就绪+内核数据拷贝到用户空间写IO=等待写事件就绪+用户空间数据拷贝到内核空间高级IO的本质:尽可能减少等的时间比重同步和异步通信同步概念:所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果异步概念:异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用还需注意多进程和多线程同步和异步之间的区别:进程/线程同步也是进程/线程之间直接的制约关系是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系. 尤其是在访问临界资源的时候。I/O多路转接之select系统提供select函数来实现多路复用输入/输出模型.select系统调用是用来让我们的程序监视多个文件描述符的状态变化的程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变select函数原型参数说明:参数nfds是需要监视的最大的文件描述符值+1;rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合;参数timeout为结构timeval,用来设置select()的等待时间参数timeout取值:NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了件;0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。fd_set结构用命令vim /usr/include/sys/select.h查看源码:select的过程:简单的select服务器将创建套接字单独封装成一个类Sock.hppselectServer.hppselect.cc下面来测试一下:在用手机测试一下:总结select就绪条件读就绪socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0;socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;监听的socket上有新的连接请求;socket上有未处理的错误写就绪socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;socket使用非阻塞connect连接成功或失败之后;socket上有未读取的错误;select特点可监控的文件描述符个数取决与sizeof(fd_set)的值.服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则服务器上支持的最大文件描述符是512*8=4096.将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select 返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。select缺点每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便.每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大select支持的文件描述符数量太小本篇文章到这就结束了,后面还会有poll,epoll的讲解。
-
【功能模块】【操作步骤&问题现象】1、同一个Java 进程内 先调用一下es服务(开启了kerberos认证)2、再调用一下Phoenix服务【截图信息】【日志信息】(可选,上传日志内容或者附件)org.apache.zookeeper.KeeperException$SystemErrorException: KeeperErrorCode = SystemError for /hbase at org.apache.phoenix.util.ServerUtil.parseServerException(ServerUtil.java:138) at org.apache.phoenix.query.ConnectionQueryServicesImpl.ensureTableCreated(ConnectionQueryServicesImpl.java:1204) at org.apache.phoenix.query.ConnectionQueryServicesImpl.createTable(ConnectionQueryServicesImpl.java:1501) at org.apache.phoenix.query.DelegateConnectionQueryServices.createTable(DelegateConnectionQueryServices.java:119) at org.apache.phoenix.schema.MetaDataClient.createTableInternal(MetaDataClient.java:2721) at org.apache.phoenix.schema.MetaDataClient.createTable(MetaDataClient.java:1114) at org.apache.phoenix.compile.CreateTableCompiler$1.execute(CreateTableCompiler.java:192) at org.apache.phoenix.jdbc.PhoenixStatement$2.call(PhoenixStatement.java:409) at org.apache.phoenix.jdbc.PhoenixStatement$2.call(PhoenixStatement.java:392) at org.apache.phoenix.call.CallRunner.run(CallRunner.java:53) at org.apache.phoenix.jdbc.PhoenixStatement.executeMutation(PhoenixStatement.java:391) at org.apache.phoenix.jdbc.PhoenixStatement.executeMutation(PhoenixStatement.java:379) at org.apache.phoenix.jdbc.PhoenixStatement.executeUpdate(PhoenixStatement.java:1811) at org.apache.phoenix.query.ConnectionQueryServicesImpl$12.call(ConnectionQueryServicesImpl.java:2573) at org.apache.phoenix.query.ConnectionQueryServicesImpl$12.call(ConnectionQueryServicesImpl.java:2536) at org.apache.phoenix.util.PhoenixContextExecutor.call(PhoenixContextExecutor.java:76) at org.apache.phoenix.query.ConnectionQueryServicesImpl.init(ConnectionQueryServicesImpl.java:2536) at org.apache.phoenix.jdbc.PhoenixDriver.getConnectionQueryServices(PhoenixDriver.java:264) at org.apache.phoenix.jdbc.PhoenixEmbeddedDriver.createConnection(PhoenixEmbeddedDriver.java:150) at org.apache.phoenix.jdbc.PhoenixDriver.connect(PhoenixDriver.java:230) at java.sql.DriverManager.getConnection(DriverManager.java:664) at java.sql.DriverManager.getConnection(DriverManager.java:208) at com.dtwave.ai.engine.freyr.strategy.phoenix.plugin.PhoenixExecutorImpl.lambda$getConnection$0(PhoenixExecutorImpl.java:123) ... 82 common frames omittedCaused by: org.apache.hadoop.hbase.client.RetriesExhaustedException: Failed after attempts=2, exceptions:2022-03-29T03:03:45.889Z, RpcRetryingCaller{globalStartTime=1648523025126, pause=50, maxAttempts=2}, org.apache.hadoop.hbase.client.RetriesExhaustedException: Failed after attempts=2, exceptions:2022-03-29T03:03:45.197Z, RpcRetryingCaller{globalStartTime=1648523025126, pause=50, maxAttempts=2}, java.io.IOException: org.apache.zookeeper.KeeperException$ConnectionLossException: KeeperErrorCode = ConnectionLoss for /hbase2022-03-29T03:03:45.888Z, RpcRetryingCaller{globalStartTime=1648523025126, pause=50, maxAttempts=2}, java.io.IOException: org.apache.zookeeper.KeeperException$SystemErrorException: KeeperErrorCode = SystemError for /hbase2022-03-29T03:03:46.041Z, RpcRetryingCaller{globalStartTime=1648523025126, pause=50, maxAttempts=2}, org.apache.hadoop.hbase.client.RetriesExhaustedException: Failed after attempts=2, exceptions:2022-03-29T03:03:45.989Z, RpcRetryingCaller{globalStartTime=1648523025939, pause=50, maxAttempts=2}, java.io.IOException: org.apache.zookeeper.KeeperException$ConnectionLossException: KeeperErrorCode = ConnectionLoss for /hbase2022-03-29T03:03:46.040Z, RpcRetryingCaller{globalStartTime=1648523025939, pause=50, maxAttempts=2}, java.io.IOException: org.apache.zookeeper.KeeperException$SystemErrorException: KeeperErrorCode = SystemError for /hbase at org.apache.hadoop.hbase.client.RpcRetryingCallerImpl.callWithRetries(RpcRetryingCallerImpl.java:152) at org.apache.hadoop.hbase.client.HBaseAdmin.executeCallable(HBaseAdmin.java:3272) at org.apache.hadoop.hbase.client.HBaseAdmin.executeCallable(HBaseAdmin.java:3264) at org.apache.hadoop.hbase.client.HBaseAdmin.tableExists(HBaseAdmin.java:472) at org.apache.phoenix.query.ConnectionQueryServicesImpl.ensureTableCreated(ConnectionQueryServicesImpl.java:1105) ... 103 common frames omittedCaused by: org.apache.hadoop.hbase.client.RetriesExhaustedException: Failed after attempts=2, exceptions:2022-03-29T03:03:45.989Z, RpcRetryingCaller{globalStartTime=1648523025939, pause=50, maxAttempts=2}, java.io.IOException: org.apache.zookeeper.KeeperException$ConnectionLossException: KeeperErrorCode = ConnectionLoss for /hbase2022-03-29T03:03:46.040Z, RpcRetryingCaller{globalStartTime=1648523025939, pause=50, maxAttempts=2}, java.io.IOException: org.apache.zookeeper.KeeperException$SystemErrorException: KeeperErrorCode = SystemError for /hbase at org.apache.hadoop.hbase.client.RpcRetryingCallerImpl.callWithRetries(RpcRetryingCallerImpl.java:152) at org.apache.hadoop.hbase.client.HTable.get(HTable.java:384) at org.apache.hadoop.hbase.client.HTable.get(HTable.java:358) at org.apache.hadoop.hbase.MetaTableAccessor.getTableState(MetaTableAccessor.java:1124) at org.apache.hadoop.hbase.MetaTableAccessor.tableExists(MetaTableAccessor.java:446) at org.apache.hadoop.hbase.client.HBaseAdmin$6.rpcCall(HBaseAdmin.java:475) at org.apache.hadoop.hbase.client.HBaseAdmin$6.rpcCall(HBaseAdmin.java:472) at org.apache.hadoop.hbase.client.RpcRetryingCallable.call(RpcRetryingCallable.java:58) at org.apache.hadoop.hbase.client.RpcRetryingCallerImpl.callWithRetries(RpcRetryingCallerImpl.java:109) ... 107 common frames omittedCaused by: java.io.IOException: org.apache.zookeeper.KeeperException$SystemErrorException: KeeperErrorCode = SystemError for /hbase at org.apache.hadoop.hbase.client.ConnectionImplementation.get(ConnectionImplementation.java:2147) at org.apache.hadoop.hbase.client.ConnectionImplementation.locateMeta(ConnectionImplementation.java:820) at org.apache.hadoop.hbase.client.ConnectionImplementation.locateRegion(ConnectionImplementation.java:787) at org.apache.hadoop.hbase.client.HRegionLocator.getRegionLocation(HRegionLocator.java:64) at org.apache.hadoop.hbase.client.RegionLocator.getRegionLocation(RegionLocator.java:58) at org.apache.hadoop.hbase.client.RegionLocator.getRegionLocation(RegionLocator.java:47) at org.apache.hadoop.hbase.client.RegionServerCallable.prepare(RegionServerCallable.java:223) at org.apache.hadoop.hbase.client.RpcRetryingCallerImpl.callWithRetries(RpcRetryingCallerImpl.java:107) ... 115 common frames omittedCaused by: org.apache.zookeeper.KeeperException$SystemErrorException: KeeperErrorCode = SystemError for /hbase at org.apache.zookeeper.KeeperException.create(KeeperException.java:97) at org.apache.zookeeper.KeeperException.create(KeeperException.java:54) at org.apache.hadoop.hbase.zookeeper.ReadOnlyZKClient$ZKTask$1.exec(ReadOnlyZKClient.java:209) at org.apache.hadoop.hbase.zookeeper.ReadOnlyZKClient.run(ReadOnlyZKClient.java:356) ... 1 common frames omitted
-
下面的懒汉式是线程不安全的,支持懒加载,因为没有加锁 synchronized,所以严格意义上它并不算单例模式。样例代码:public class Singleton{ private static Singleton instance; private Singleton(){ } public static Singleton getInstance(){ if(instance == null){ return new Singleton(); } return instance; }}
-
python中的多进程模块,因为使用进程池,调用进程池和进程池的队列multiprocessing.Pool可以使用,不会出现运行失败但是使用进程池之间的队列通信multiprocessing.Manager().Queue(),为什么使用就报错?在自己的电脑运行正常这个不是python内置的吗?有谁知道原因吗?
-
【问题现象】扩容后新的节点监控不显示【问题分析】1、查看扩容节点的dms_agent进程和agent_service进程是否正常2、查看老节点的agent_service.log,日志中报错agent_service未拉起【解决方案】若无法明确哪些节点的agent_service进程未被拉起,kill掉所有agent_service进程后等待自动重新拉起gs_ssh -c "ps -ef | grep agent_service | grep -v grep | awk '{print $2}' | xargs kill -9"
-
IPC在这之前我们先简单说一下IPC,IPC是Inter-Process Communication的缩写,是进程间通信或者跨进程通信的意思,既然说到进程,大家要区分一下进程和线程,进程一般指的是一个执行单元,它拥有独立的地址空间,也就是一个应用或者一个程序。线程是CPU调度的最小单元,是进程中的一个执行部分或者说是执行体,两者之间是包含与被包含的关系。因为进程间的资源不能共享的,所以每个系统都有自己的IPC机制,Android是基于Linux内核的移动操作系统,但它并没有继承Linux的IPC机制,而是有着自己的一套IPC机制。BinderBinder就是Android中最具特色的IPC方式,AIDL其实就是通过Binder实现的,因为在我们定义好aidl文件后,studio就帮我们生成了相关的Binder类。事实上我们在使用AIDL时候继承的Stub类,就是studio帮我们生成的Binder类,所以我们可以通过查看studio生成的代码来了解Binder的工作原理。首先我们定义一个AIDL文件
上滑加载中
推荐直播
-
华为云码道 × 仓颉编程:工程化AI编码探索2026/05/27 周三 19:00-21:00
刘俊杰-华为云仓颉语言专家/李炎-华为云码道技术专家/王智鹏-OpenCangjie开源社区发起人
本场直播围绕华为云仓颉语言与华为云码道的深度结合,展示华为云智能编程从零基础到高效落地的完整生态能力。以华为云码道为引擎,仓颉语言为载体,带给大家日常提效、趣味创新到极速量产的开发体验。
回顾中
热门标签