• [集群&DWS] GaussDB(DWS)stream线程池设计(二)
    一、数据结构设计stream线程池采用无锁队列实现,其目的是为了高效管理线程的出/入池操作。定义结构体ThreadSlot保存线程池中每一个线程的信息,包含:线程状态、线程号、线程对应的database oid、线程执行所需的信息StreamProducer,StreamProducer是父线程向子线程传递的唯一结构、线程唤醒所需的锁和条件变量。如下所示:typedef struct{       int status;       uint32 idx;       ThreadId tid;       Oid dbOid;       StreamProducer* streamObj;       pthread_mutex_t m_mutex;       pthread_cond_t m_cond;} ThreadSlot;定义结构体StreamThreadPool表征线程池,结构如下所示:class StreamThreadPool: public BaseObject{public:    StreamThreadPool();    void Init(int num);                             // StreamThreadPool init       int Call(StreamProducer* obj);                   // 获取idle线程 或 create 新线程    bool Wait();                                  // idle线程等待唤醒或者超时退出    ThreadSlot* GetLocalSlot();                     // get streamThreadSlot    void SetLocalSlot(int slotIdx);                   // set streamThreadSlot    StreamPool* GetLocalPool();                    // 获取streamDBPool 或 新建一个    ThreadSlot* PopSlot();                         // 从idleRing/emptyRing获取一slot    void PushToEmpty(ThreadSlot* slot);             // 将slot直接放入emptyRing    void PushToIdle(StreamPool* pool, ThreadSlot* slot); //将slot直接放入idleRing    void LocalPushToIdle();                        // 根据状态,将slot放入idleRing    void LocalPushToEmpty();                      // 根据状态,将slot放入emptyRing    int CleanStreamPool(const char *dbName, cleanOption cleanMode); //根据db信息清线程    void CleanInAllStreamPool(int desNum);           // 调整线程池中stream线程个数    int GetStreamNum();                           // 获取线程池中stream线程个数    bool Release();                                // 判断超时线程是否需要清理    bool TimeoutClean();                                // 清理超时idle线程private:    int size;    ThreadSlot* threadSlots;    ArrayLockFreeQueue emptyRing;    StreamPool* PoolListHead;}结构体中,size表示线程池中拟预留的ThreadSlot个数,ThreadSlot被保存在threadSlots数组中;无锁队列emptyRing用来保存未创建线程的ThreadSlot,对应地,idleRing用来保存空闲的已创建stream线程的ThreadSlot。由于stream线程的初始化信息和database是强相关的,如果不保留database相关的信息,那么线程初始化的时间代价仍然较高,所以线程池中的线程复用时,需要满足database信息匹配,所以一个emptyRing和一个database相匹配,保存在链表PoolListHead中,链表元素StreamPool对应结构如下:typedef struct StreamPool{       Oid dbOid;       ArrayLockFreeQueue idleRing;       struct StreamPool* next;} StreamPool;线程池中各结构间组织的直观图如下所示:图一、基于无锁队列的线程池结构上图中threadSlots可以放在idleRing(蓝色)、emptyRing(绿色)和运行空间(黄色)中,具体在下节介绍。二、stream线程状态转移DFA设计每一个记录线程信息的结构ThreadSlot中都保存了线程当前的状态status,记录线程状态的目的是为了保障线程执行过程的有序控制,也可以通过状态的互斥避免threadSlot不会被两个线程同时使用。stream线程状态转移用确定性有限状态机(DFA,definite automata)表征,共包含4个状态:STREAM_SLOT_EXIT、STREAM_SLOT_IDLE、STREAM_SLOT_HOLD和STREAM_SLOT_RUN状态。其物理含义如下:STREAM_SLOT_EXIT:线程退出状态,表示线程未被创建或线程已退出;STREAM_SLOT_IDLE:线程可复用状态,表示线程在idleRing中,可以被复用;STREAM_SLOT_HOLD:线程临时独占状态,表示线程在做进入下一个状态的准备工作;STREAM_SLOT_RUN:线程运行状态,表示线程正在执行任务。状态间转移条件如下所示,图中粗箭头表示状态机主循环部分:图二、stream线程状态转移与状态对应的,是slot所处的位置,slot所处的位置有三处,分别是idleRing、emptyRing和运行空间,slot从无锁队列中拿出,运行时所处的位置,我们称之为运行空间。各状态所处的位置情况如下所示:STREAM_SLOT_EXIT:idleRing(idle线程超时)、emptyRing(初始化或者FATAL);STREAM_SLOT_IDLE:idleRingSTREAM_SLOT_HOLD:运行空间(从无锁队列中取出)、idleRing(idle线程超时或中断);STREAM_SLOT_RUN:运行空间。Slot的位置变化和状态转移的关系如下,图中粗箭头表示状态机主循环部分:图三、stream线程状态转移和slot位置的转移关系根据各状态所处的位置情况,从idleRing中取出的slot可能有三种状态:EXIT、IDLE、HOLD。当取出IDLE状态的slot,说明线程可复用;当取出EXIT状态的slot,说明线程已退出,此时需要将slot转存到emptyRing;当取出HOLD状态,说明线程正在被使用,此时需要放回idleRing。EmptyRing中slot的状态只能是EXIT,运行空间中slot的状态要么是HOLD(刚取出还未运行),要么是RUN(正在运行),不再赘述。
  • [集群&DWS] GaussDB(DWS) 集群通信系列三:集群通信常用视图
    视图是检测数据库运行状态的重要工具和手段,GaussDB(DWS)集群通信常用视图主要包含pg_stat_activity、pg_comm_client、pgxc_thread_wait_status、pgxc_comm_recv_stream、pgxc_comm_send_stream、pgxc_comm_status,其在数据库通信问题定位中发挥了重要作用。1、pg_stat_activity该视图显示和当前用户查询相关的信息。主要用于查看当前用户执行查询的状态和查询对应的query_id,2、pg_comm_client_info该视图存储单个节点客户端连接信息(DN上查询该视图显示CN连接DN的信息)。3、pgxc_thread_wait_status该视图显示由执行语句产生的线程之间层次调用关系,以及各个线程的阻塞等待状态。该视图常用来定位数据库通信过程中的hang问题,主要用于定位的信息包括:Ø  query_id:查询IDØ  tlevel:线程层级,对于集群通信而言,Postgres thread线程为0级线程,其根据任务会fork其他子线程,集群通信线程间的关系具体可以参考http://3ms.huawei.com/hi/group/2191/wiki_5275953.html?for_statistic_from=all_group_wikiØ  wait_status:等待线程的当前等待状态,none表示没有等待,其他状态可参考产品文档。4、pgxc_comm_recv_stream该视图显示DN上所有的通信库接收流的状态。5、pgxc_comm_send_stream该视图显示DN上所有的通信库发送流的状态。6、pgxc_comm_status该视图显示所有DN的通信库状态。包含:节点名称、节点通信库发送/接收速率、cmailbox的buffer大小、libcomm/libpq进程通信内存大小、线程实时/最高实时使用率、当前使用的逻辑连接总数。通信视图的关联关系使用和hang问题定位示例查询单个的视图往往信息有限而作用不大,视图最大的妙用在于利用各信息的关联关系,要求使用者能够联系多方信息,进行问题定位和状态监测,这一点无疑比较困难,因此整理和介绍视图的关联关系对于视图使用者极为重要,此处仅通过hang问题的示例,和视图部分信息的关联关系使用,介绍这种运用思想和方法,通信视图完整的关联关系将在后续GaussDB(DWS) 集群通信系列中给出。可以看出,定位hang问题的一般步骤为:1、  根据pg_stat_activity视图查看当前查询的query_id。2、  根据pgxc_thread_wait_status中相应的query_id查询对应的线程状态,是等待什么导致的hang3、  根据wait_status的信息,查看对端信息,包含线程层级,节点等4、  查看对端具体的线程信息pg_thread_wait_status,如果是DN可查对应的连接流信息,可以利用netstat,线程号查看对端信息,分析对端的行为。通信hang问题典型案例:
  • [技术干货] Cassandra copy命令使用指南
    copy是cqlsh中的逻辑导入导出命令。包括两个命令COPY TO / COPY FROM.使用这组命令可以在Cassandra与其他RDBMS或Cassandra之间迁移数据。目前已经支持csv, json文件格式以及标准输出和输入。 1. 如何执行copy?    1. 执行copy时,首先要启动cqlsh. 启动cqlsh的方法见 如何使用cqlsh访问Cassandra。启动cqlsh后,接着执行copy命令。如下1. /cqlsh  127.0.0.1 9042 -u {user_name} -p {password}1COPY cycling.cyclist_name TO  '/home/cas/copydata' ;2. 如果数据量很大,可以放在后台执行。使用cqlsh的-e 参数。-e 参数用来后台执行给定的语句,然后退出。命令如下1. /cqlsh  127.0.0.1 9042 -u {user_name} -p {password} -e  "COPY cycling.cyclist_name TO '/home/cas/copydata'" ;2. COPY TO   1. /cqlsh  127.0.0.1 9042 -e  "COPY cycling.cyclist_name to '/home/cas/copydata'"COPY TO 常见的参数有: NUMPROCESSES, RATEFILE, PAGESIZE, BEGINTOKEN, ENDTOKEN, MAXATTEMPTS, MAXOUTPUTSIZE; 新增的参数有: RESULTFILE, DATAFORMATS, WHERECONDITION.重点介绍常见和新增的参数,关于其他COPY TO的参数可以参考Cassandra官网文档: copy-to1. `file name` 导出时,可以指定文件名称为目录, 也可以指定文件名称为文件。默认导出到文件。如果指定导出文件为存在的目录时,将会按照range范围导出到目录下的不同的文件中,如果某一个range范围导出没有数据,则不会生成文件。如果指定的文件名是文件,那将会导出到指定的文件中。如果文件不存在,则新创建该文件。1. /cqlsh  127.0.0.1 9042 -e  "COPY cycling.cyclist_name to '/home/cas/copydata'"1. /cqlsh  127.0.0.1 9042 -e  "COPY cycling.cyclist_name to '/home/cas/copydata/cycling.cyclist_name'"2. `NUMPROCESSES` 导出的线程数。在导出时,会将range范围进行细分。导出的线程数越多,细分range的范围数也就越多。但当线程数太多的时候,容易对服务端造成压力,造成导出失败。所以要选择一个合适的线程数进行导出,默认的线程个数是(CPU核数 - 1)3.  `RATEFILE` 速率文件。指定文件路径后,会打印导出过程的瞬时速率,可以用来评价导出的性能。4.  `PAGESIZE` 在一次page查询中。获取的row数。 默认为 1000。 建议不要设的调小。设置太小会影响导出性能。5.  `BEGINTOKEN, ENDTOKEN` 执行要导出的range范围。默认是全部数据导出。6.  `MAXATTEMPTS` 每一个查询重试的次数。如果某次查询达到最大重试次数,那么导出将会直接失败。 7.  `MAXOUTPUTSIZE` 每一个导出文件中的最大的行数。如果导出数据大于这个值。将会轮转生成另一个文件。默认是不限制的。1. /cqlsh  127.0.0.1 9042 -e  "COPY cycling.cyclist_name to '/home/cas/copydata/cycling.cyclist_name' with MAXOUTPUTSIZE=1"8.  `RESULTFILE` 导出结果文件路径,如果不设置。默认生成在当前执行目录下。如果执行目录中存在之前的结果文件,将会重命名之前的结果文件。导出结果的内容主要包括:导出成功与否,总共导出的行数和速率,导出的range范围数,以及成功失败的range范围数,并展示每一个range范围的结果和导出的行数。9. `DATAFORMATS` 导出的数据格式。取值为csv, json。默认为csv格式。指定为json时,导出为json格式的数据。10. `WHERECONDITION` 导出的查询条件。支持查询条件的导出。对于非主键列,如果通过索引来查询,提高导出性能。         1. wherecondition 格式如下 `[key(operators)value,key(operators)value...]`        2. operators 支持 `[">=", "<=", ">", "<", "="]`,例如: `'keyspace_name=system,table_name>=local'`        3. 如果value中包含有特殊字符,比如保留字符 【" ,><=’】, 那么需要给字符加上双引号,例如:`'keyspace_name=system,table_name>="loc>=al"'`一个完整的COPY TO例子:1nohup  . /cqlsh  127.0.0.1 9042 --request-timeout=3600 --debug -e  "COPY nihao.sz_user to '/home/cas/copydata' with WHERECONDITION='update_timestamp=1' NUMPROCESSES=12 AND RATEFILE='rate.txt' AND RESULTFILE='export_result' AND dataformats='json';"   > export .log 2>&1 &3. COPY FROM   1COPY <table name> [(<column>, ...)] FROM < file  name> WITH <copy option> [AND <copy option> ...]COPY FROM 常见的参数有: NUMPROCESSES, MAXROWS, INGESTRATE, ERRFILE, MAXBATCHSIZE, MINBATCHSIZE, CHUNKSIZE, MAXPARSEERRORS, MAXINSERTERRORS, SKIPROWS, SKIPCOLS;新增的参数有: DATAFORMATS.重点介绍常见和新增的参数,关于其他COPY FROM参数可以参考Cassandra官网文档: copy-from1. `file name` 导入数据时,指定导入的文件路径,可以是一个目录,或者是一个文件,或者是逗号分隔的文件名列表。如果是目录,则导入该目录下的所有文件。2. `NUMPROCESSES` 导入的线程数3. `MAXROWS` 导入的最大行数。默认不限制。4. `INGESTRATE` 每一秒导入的最大行数。默认为 1000005. `ERRFILE` 导入失败的列将放在这个文件中。6. `MAXBATCHSIZE` 每一次batch导入的最大行数。默认为207. `MINBATCHSIZE` 每一次batch导入的最小行数。默认为28. `CHUNKSIZE` 导入主线程给子线程每次传递的行数,默认为 10009. `MAXPARSEERRORS` 可以忽略的语法解析错误的最大行数。默认不限制10. `MAXINSERTERRORS` 可以忽略的插入失败的最大行数。默认为100011. `SKIPROWS` 导入时初始跳过的行数。默认为0,不跳过12. `SKIPCOLS` 导入时忽略的列名,以逗号分隔。默认不忽略。13. `DATAFORMATS` 导入的数据格式。取值为csv,json。默认为csv格式。数据为json格式时,必须指定为json。一个完整的COPY FROM例子:1nohup  . /cqlsh  127.0.0.1 9042 --request-timeout=3600 --debug -e  "COPY nihao.sz_user FROM '/home/cas/copydata' with NUMPROCESSES=12 AND RATEFILE='rate.txt' AND dataformats='json';"   > import .log 2>&1 &
  • [其他] 【扩容】重分步进程意外退出
    问题现象:重分步进程意外退出检查是否有会话:select coorname, usename, client_addr, query_start,sysdate-query_start as dur, state, enqueue,waiting, pid,query_id, substr(query,1,200),datname from pgxc_stat_activity where application_name = 'gs_redis' and state = 'active';恢复方案:python命令拉起,在主cn上执行/opt/dws/package/script/gs_expand -t redistribute --fast-redis --redis-mode=insert --dws-mode --parallel-jobs=24 > gs_expand.log 2>&1 &
  • [交流吐槽] JVM原理学习总结
    这篇总结主要是基于我之前JVM系列文章而形成的的。主要是把重要的知识点用自己的话说了一遍,可能会有一些错误,还望见谅和指点。谢谢#更多详细内容可以查看我的专栏文章:深入理解JVM虚拟机https://blog.csdn.net/column/details/21960.htmlJVM介绍和源码首先JVM是一个虚拟机,当你安装了jre,它就包含了jvm环境。JVM有自己的内存结构,字节码执行引擎,因此class字节码才能在jvm上运行,除了Java以外,Scala,groovy等语言也可以编译成字节码而后在jvm中运行。JVM是用c开发的。JVM内存模型内存模型老生常谈了,主要就是线程共享的堆区,方法区,本地方法栈。还有线程私有的虚拟机栈和程序计数器。堆区存放所有对象,每个对象有一个地址,Java类jvm初始化时加载到方法区,而后会在堆区中生成一个Class对象,来负责这个类所有实例的实例化。栈区存放的是栈帧结构,栈帧是一段内存空间,包括参数列表,返回地址,局部变量表等,局部变量表由一堆slot组成,slot的大小固定,根据变量的数据类型决定需要用到几个slot。方法区存放类的元数据,将原来的字面量转换成引用,当然,方法区也提供常量池,常量池存放-128到127的数字类型的包装类。字符串常量池则会存放使用intern的字符串变量。JVM OOM和内存泄漏这里指的是oom和内存泄漏这类错误。oom一般分为三种,堆区内存溢出,栈区内存溢出以及方法区内存溢出。堆内存溢出主要原因是创建了太多对象,比如一个集合类死循环添加一个数,此时设置jvm参数使堆内存最大值为10m,一会就会报oom异常。栈内存溢出主要与栈空间和线程有关,因为栈是线程私有的,如果创建太多线程,内存值超过栈空间上限,也会报oom。方法区内存溢出主要是由于动态加载类的数量太多,或者是不断创建一个动态代理,用不了多久方法区内存也会溢出,会报oom,这里在1.7之前会报permgem oom,1.8则会报meta space oom,这是因为1.8中删除了堆中的永久代,转而使用元数据区。内存泄漏一般是因为对象被引用无法回收,比如一个集合中存着很多对象,可能你在外部代码把对象的引用置空了,但是由于对象还被集合给引用着,所以无法被回收,导致内存泄漏。测试也很简单,就在集合里添加对象,添加完以后把引用置空,循环操作,一会就会出现oom异常,原因是内存泄漏太多了,导致没有空间分配新的对象。常见调试工具命令行工具有jstack jstat jmap 等,jstack可以跟踪线程的调用堆栈,以便追踪错误原因。jstat可以检查jvm的内存使用情况,gc情况以及线程状态等。jmap用于把堆栈快照转储到文件系统,然后可以用其他工具去排查。visualvm是一款很不错的gui调试工具,可以远程登录主机以便访问其jvm的状态并进行监控。class文件结构class文件结构比较复杂,首先jvm定义了一个class文件的规则,并且让jvm按照这个规则去验证与读取。开头是一串魔数,然后接下来会有各种不同长度的数据,通过class的规则去读取这些数据,jvm就可以识别其内容,最后将其加载到方法区。JVM的类加载机制jvm的类加载顺序是bootstrap类加载器,extclassloader加载器,最后是appclassloader用户加载器,分别加载的是jdk/bin ,jdk/ext以及用户定义的类目录下的类(一般通过ide指定),一般核心类都由bootstrap和ext加载器来加载,appclassloader用于加载自己写的类。双亲委派模型,加载一个类时,首先获取当前类加载器,先找到最高层的类加载器bootstrap让他尝试加载,他如果加载不了再让ext加载器去加载,如果他也加载不了再让appclassloader去加载。这样的话,确保一个类型只会被加载一次,并且以高层类加载器为准,防止某些类与核心类重复,产生错误。defineclass findclass和loadclass类加载classloader中有两个方法loadclass和findclass,loadclass遵从双亲委派模型,先调用父类加载的loadclass,如果父类和自己都无法加载该类,则会去调用findclass方法,而findclass默认实现为空,如果要自定义类加载方式,则可以重写findclass方法。常见使用defineclass的情况是从网络或者文件读取字节码,然后通过defineclass将其定义成一个类,并且返回一个Class对象,说明此时类已经加载到方法区了。当然1.8以前实现方法区的是永久代,1.8以后则是元空间了。JVM虚拟机字节码执行引擎jvm通过字节码执行引擎来执行class代码,他是一个栈式执行引擎。这部分内容比较高深,在这里就不献丑了。编译期优化和运行期优化编译期优化主要有几种1 泛型的擦除,使得泛型在编译时变成了实际类型,也叫伪泛型。2 自动拆箱装箱,foreach循环自动变成迭代器实现的for循环。3 条件编译,比如if(true)直接可得。运行期优化主要有几种1 JIT即时编译Java既是编译语言也是解释语言,因为需要编译代码生成字节码,而后通过解释器解释执行。但是,有些代码由于经常被使用而成为热点代码,每次都编译太过费时费力,干脆直接把他编译成本地代码,这种方式叫做JIT即时编译处理,所以这部分代码可以直接在本地运行而不需要通过jvm的执行引擎。2 公共表达式擦除,就是一个式子在后面如果没有被修改,在后面调用时就会被直接替换成数值。3 数组边界擦除,方法内联,比较偏,意义不大。4 逃逸分析,用于分析一个对象的作用范围,如果只局限在方法中被访问,则说明不会逃逸出方法,这样的话他就是线程安全的,不需要进行并发加锁。1JVM的垃圾回收1 GC算法:停止复制,存活对象少时适用,缺点是需要两倍空间。标记清除,存活对象多时适用,但是容易产生随便。标记整理,存活对象少时适用,需要移动对象较多。2 GC分区,一般GC发生在堆区,堆区可分为年轻代,老年代,以前有永久代,现在没有了。年轻代分为eden和survior,新对象分配在eden,当年轻代满时触发minor gc,存活对象移至survivor区,然后两个区互换,等待下一场gc,当对象存活的阈值达到设定值时进入老年代,大对象也会直接进入老年代。老年代空间较大,当老年代空间不足以存放年轻代过来的对象时,开始进行full gc。同时整理年轻代和老年代。一般年轻代使用停止复制,老年代使用标记清除。3 垃圾收集器serial串行parallel并行它们都有年轻代与老年代的不同实现。然后是scanvage收集器,注重吞吐量,可以自己设置,不过不注重延迟。cms垃圾收集器,注重延迟的缩短和控制,并且收集线程和系统线程可以并发。cms收集步骤主要是,初次标记gc root,然后停顿进行并发标记,而后处理改变后的标记,最后停顿进行并发清除。g1收集器和cms的收集方式类似,但是g1将堆内存划分成了大小相同的小块区域,并且将垃圾集中到一个区域,存活对象集中到另一个区域,然后进行收集,防止产生碎片,同时使分配方式更灵活,它还支持根据对象变化预测停顿时间,从而更好地帮用户解决延迟等问题。
  • [交流吐槽] JVM原理学习总结
    这篇总结主要是基于我之前JVM系列文章而形成的的。主要是把重要的知识点用自己的话说了一遍,可能会有一些错误,还望见谅和指点。谢谢#更多详细内容可以查看我的专栏文章:深入理解JVM虚拟机https://blog.csdn.net/column/details/21960.htmlJVM介绍和源码首先JVM是一个虚拟机,当你安装了jre,它就包含了jvm环境。JVM有自己的内存结构,字节码执行引擎,因此class字节码才能在jvm上运行,除了Java以外,Scala,groovy等语言也可以编译成字节码而后在jvm中运行。JVM是用c开发的。JVM内存模型内存模型老生常谈了,主要就是线程共享的堆区,方法区,本地方法栈。还有线程私有的虚拟机栈和程序计数器。堆区存放所有对象,每个对象有一个地址,Java类jvm初始化时加载到方法区,而后会在堆区中生成一个Class对象,来负责这个类所有实例的实例化。栈区存放的是栈帧结构,栈帧是一段内存空间,包括参数列表,返回地址,局部变量表等,局部变量表由一堆slot组成,slot的大小固定,根据变量的数据类型决定需要用到几个slot。方法区存放类的元数据,将原来的字面量转换成引用,当然,方法区也提供常量池,常量池存放-128到127的数字类型的包装类。字符串常量池则会存放使用intern的字符串变量。JVM OOM和内存泄漏这里指的是oom和内存泄漏这类错误。oom一般分为三种,堆区内存溢出,栈区内存溢出以及方法区内存溢出。堆内存溢出主要原因是创建了太多对象,比如一个集合类死循环添加一个数,此时设置jvm参数使堆内存最大值为10m,一会就会报oom异常。栈内存溢出主要与栈空间和线程有关,因为栈是线程私有的,如果创建太多线程,内存值超过栈空间上限,也会报oom。方法区内存溢出主要是由于动态加载类的数量太多,或者是不断创建一个动态代理,用不了多久方法区内存也会溢出,会报oom,这里在1.7之前会报permgem oom,1.8则会报meta space oom,这是因为1.8中删除了堆中的永久代,转而使用元数据区。内存泄漏一般是因为对象被引用无法回收,比如一个集合中存着很多对象,可能你在外部代码把对象的引用置空了,但是由于对象还被集合给引用着,所以无法被回收,导致内存泄漏。测试也很简单,就在集合里添加对象,添加完以后把引用置空,循环操作,一会就会出现oom异常,原因是内存泄漏太多了,导致没有空间分配新的对象。常见调试工具命令行工具有jstack jstat jmap 等,jstack可以跟踪线程的调用堆栈,以便追踪错误原因。jstat可以检查jvm的内存使用情况,gc情况以及线程状态等。jmap用于把堆栈快照转储到文件系统,然后可以用其他工具去排查。visualvm是一款很不错的gui调试工具,可以远程登录主机以便访问其jvm的状态并进行监控。class文件结构class文件结构比较复杂,首先jvm定义了一个class文件的规则,并且让jvm按照这个规则去验证与读取。开头是一串魔数,然后接下来会有各种不同长度的数据,通过class的规则去读取这些数据,jvm就可以识别其内容,最后将其加载到方法区。JVM的类加载机制jvm的类加载顺序是bootstrap类加载器,extclassloader加载器,最后是appclassloader用户加载器,分别加载的是jdk/bin ,jdk/ext以及用户定义的类目录下的类(一般通过ide指定),一般核心类都由bootstrap和ext加载器来加载,appclassloader用于加载自己写的类。双亲委派模型,加载一个类时,首先获取当前类加载器,先找到最高层的类加载器bootstrap让他尝试加载,他如果加载不了再让ext加载器去加载,如果他也加载不了再让appclassloader去加载。这样的话,确保一个类型只会被加载一次,并且以高层类加载器为准,防止某些类与核心类重复,产生错误。defineclass findclass和loadclass类加载classloader中有两个方法loadclass和findclass,loadclass遵从双亲委派模型,先调用父类加载的loadclass,如果父类和自己都无法加载该类,则会去调用findclass方法,而findclass默认实现为空,如果要自定义类加载方式,则可以重写findclass方法。常见使用defineclass的情况是从网络或者文件读取字节码,然后通过defineclass将其定义成一个类,并且返回一个Class对象,说明此时类已经加载到方法区了。当然1.8以前实现方法区的是永久代,1.8以后则是元空间了。JVM虚拟机字节码执行引擎jvm通过字节码执行引擎来执行class代码,他是一个栈式执行引擎。这部分内容比较高深,在这里就不献丑了。编译期优化和运行期优化编译期优化主要有几种1 泛型的擦除,使得泛型在编译时变成了实际类型,也叫伪泛型。2 自动拆箱装箱,foreach循环自动变成迭代器实现的for循环。3 条件编译,比如if(true)直接可得。运行期优化主要有几种1 JIT即时编译Java既是编译语言也是解释语言,因为需要编译代码生成字节码,而后通过解释器解释执行。但是,有些代码由于经常被使用而成为热点代码,每次都编译太过费时费力,干脆直接把他编译成本地代码,这种方式叫做JIT即时编译处理,所以这部分代码可以直接在本地运行而不需要通过jvm的执行引擎。2 公共表达式擦除,就是一个式子在后面如果没有被修改,在后面调用时就会被直接替换成数值。3 数组边界擦除,方法内联,比较偏,意义不大。4 逃逸分析,用于分析一个对象的作用范围,如果只局限在方法中被访问,则说明不会逃逸出方法,这样的话他就是线程安全的,不需要进行并发加锁。1JVM的垃圾回收1 GC算法:停止复制,存活对象少时适用,缺点是需要两倍空间。标记清除,存活对象多时适用,但是容易产生随便。标记整理,存活对象少时适用,需要移动对象较多。2 GC分区,一般GC发生在堆区,堆区可分为年轻代,老年代,以前有永久代,现在没有了。年轻代分为eden和survior,新对象分配在eden,当年轻代满时触发minor gc,存活对象移至survivor区,然后两个区互换,等待下一场gc,当对象存活的阈值达到设定值时进入老年代,大对象也会直接进入老年代。老年代空间较大,当老年代空间不足以存放年轻代过来的对象时,开始进行full gc。同时整理年轻代和老年代。一般年轻代使用停止复制,老年代使用标记清除。3 垃圾收集器serial串行parallel并行它们都有年轻代与老年代的不同实现。然后是scanvage收集器,注重吞吐量,可以自己设置,不过不注重延迟。cms垃圾收集器,注重延迟的缩短和控制,并且收集线程和系统线程可以并发。cms收集步骤主要是,初次标记gc root,然后停顿进行并发标记,而后处理改变后的标记,最后停顿进行并发清除。g1收集器和cms的收集方式类似,但是g1将堆内存划分成了大小相同的小块区域,并且将垃圾集中到一个区域,存活对象集中到另一个区域,然后进行收集,防止产生碎片,同时使分配方式更灵活,它还支持根据对象变化预测停顿时间,从而更好地帮用户解决延迟等问题。
  • [其他] 外表导入40G数据耗时1小时
    【问题现象】外表导入40G数据耗时1小时,性能裂化严重【排查方法】语句执行慢,打印执行计划发现慢在stream上。使用gsar发现业务卡持续有重传。netstat -anop|grep “on (”|grep -v “/0/0”|sort -rnk3|head发现确实有超过一次的重传。均衡网卡绑核,没有效果,重传仍比较严重。测试ping时延和重传率有较大关联。查看重传对端节点top,ksoftirq仍很高。BIOS上SMMU确认是关闭的,重启过服务器的节点ksoftirq低,未重启的ksoftirq高。cat /proc/softirqs | awk '{print $1,$10,$11}' ksoftirq计数增长较慢,但CPU占用高。while true;do date;cat /proc/112/stack;echo;done > stack.log 抓取CPU高的ksoftirq进程堆栈,栈都在switch_to函数,未见异常。perf record -p 进程号 -ag sleep 10perf report抓CPU高的ksoftirq进程的热点函数,发现packet_rcv和run_filter函数异常,run_filter由packet_rcv调用,一般packet_rcv不是tcp连接用到的函数cat /proc/net/ptype 没有packet_rcv函数显示tcpdump !tcp -i nic0 抓包未发现异常cat /proc/net/packet发现有四千多结果,通过结果iNode查询lsof -P -n | grep inode 发现是dhclient进程。【原因分析】收包时,每个包都要经过所有的dhclient进程,过多的dhclient进程导致软中断数不多而CPU使用率高。【处理方法】排查定时任务,找到是Ruby用户执行的netcardMonitor会导致dhclient持续增长。每次service network restart都会失败并增加一个dhclient进程,注释掉重启网络服务规避。 
  • [其他] idle线程未清理
    使用clean connection to all for database dbname;清理后仍可以看到idle连接。解决方法:clean connection to all force for database dbname;执行后idle线程全部清理
  • [软件平台] 【MDC300产品】【惯导功能】导远570D惯导抽象是走UART还是CAN
    【功能模块】惯导数据抽象【操作步骤&问题现象】1、导远570D惯导通过UART连接到MDC300,并启动ins惯导数据抽象进程2、ins惯导数据抽象没有发出数据3、用mdc-tool监控UART确定MCU是有收到惯导发来的串口数据并转发给HOST的4、用MCD Tool工具查看发现InsCmHostProcess进程通过someip接收的是CAN数据而非UART数据问题:ins惯导数据抽象到底是接收CAN数据还是UART数据?(MDC的软件版本为105)【截图信息】【日志信息】(可选,上传日志内容或者附件)
  • [问题求助] 【MDC300产品】【惯导功能】570D惯导抽象是走UART还是CAN
    【功能模块】【操作步骤&问题现象】1、导远570D惯导通过UART连接到MDC300,并启动ins惯导数据抽象进程2、ins惯导数据抽象没有发出数据3、用mdc-tool监控UART确定MCU是有收到串口数据并转发给HOST的4、用MCD工具查看发现InsCmHostProcess进程通过someip接收的是CAN数据而非UART数据问题:ins惯导数据抽象到底是接收CAN还是UART?【截图信息】【日志信息】(可选,上传日志内容或者附件)
  • [参赛经验分享] 2019华为云数据库TaurusDB性能挑战赛亚军参赛总结
    缘起TaurusDB是华为云自主研发的最新一代云原生分布式数据库,完全兼容mysql8.0,采用计算与存储分离、日志即数据的架构设计,支持1写15读,性能达到原生Mysql的7倍。Taurus构建在共享分布式存储上,存储空间最高达128T,能跨AZ部署,具有可靠、高性能、易伸缩等特性。华为云TaurusDB性能挑战赛是由华为云主办的数据库领域顶级赛事,大赛将综合科研教学成果及商业领域需求,探索数据库领域的技术问题的可行性,为需求方和开发者提供联接的桥梁;并联合合作伙伴,搭建一个技术交流、人才培养、机遇共创数据库开发者平台和生态。听起来很激动人心吧,关键是奖金也很多, 同时还能刷脸! 最早是听一个同事说起的,恰好本人的工作也是“新一代数据库”开发,看着这么多人工智能的比赛不能参加,只能后悔自己选错了专业,但是机会来了,终于有自己擅长的领域了,心想不能错过这次机会,一边默默加了收藏。比赛内容一句话概括,就是实现一个kvstore,初赛是单机引擎,复赛要求存储计算分离,并且kv都是定长的,如果通用性做得好,可以直接用作块存储。题目初赛和复赛稍有区别,这里只说复赛的题目。存储引擎K,V都是定长的,key 8bytes, value 4K bytes。 测试分为三轮,第一轮, 16个线程写,每个线程写400万次;第二轮,16个线程读,每个线程按顺序读取400万次;第三轮,16个线程读,每个线程逆序读取400万次。第三轮的逆序是局部随机,总体逆序,也即在10M的数据范围内随机读,然后以10M为单位逆序推进。 成绩就是看总时间,时间越短越好。前面已经说过,初赛要实现的是单机引擎,复赛要实现的是存储计算分离的引擎,最大的限制是计算节点不能持久化任何数据。预选赛不得不说,华为可真会玩,这次预选赛竟然还要做题,并且是必须要学习他们的资料才能通过的题目,题目包括数据库的基本常识,但是也有产品介绍,经过深夜两个小时的学习,我算是第一次知道了这么多种不同的产品究竟是干嘛的。第一次答题还错了一道,以防万一,又做了一遍,这次错了两道,算了不玩了,95分也够了,睡觉去。但是心里总是不舒服,究竟是哪错了,又看了一遍,原来是幻读的理解这道题错了。初赛不出意外,预选赛顺利通过,初赛就开始写代码了,初赛只要写对差不多就可以晋级复赛。 当然任何一个程序员都不会满足刚好够用的状态,因此正常能想到的优化都加上了: 比如Direct IO, 多文件做数据分区,引入写buffer,比较频繁修改元数据,读取使用自己实现的page cache,以免4K读不能打满磁盘带宽。复赛复赛要求存储计算分离, 我认真做了一些分析,同时也对他们的硬件做了测试,分析下来,这个比赛比拼的点和我平时在工作中要追求的还是很不一样的。 平时无论做什么系统,都是在延迟差不多的情况下,把吞吐量做高,这次比赛最关键的一个点是:延迟第一,延迟是最重要的,特别是写的阶段,因为并发只有16,想通过聚合换吞吐量都不好使了。首先考虑持久化方法:4K恰好能对齐IO,所以key和value要分开存储,      想使用rocksdb之类的存储引擎即使不被禁止也是没有竞争力的。value非常随机,基本不用考虑压缩,就算有一点点好处,实现起来也太复杂了。再次考虑如何优化IO:SSD的IO吞吐量高于4K * iops, 不管是读还是写,IO聚合是必须的。单文件会遇到文件系统瓶颈,需要多文件, 也即要对数据做partition。关于同步IO还是异步IO的选择, 因为延迟优先,所以应该选同步IO。最后考虑网络框架:首先,我直接放弃考虑任何已有的网络框架,因为这是benchmark, 再小的开销也是开销,都会让程序变慢。其次,实际上任何IO      multiplexing的框架都会造成额外的延迟,不仅复杂,也是得不尝失的。综上,我决定就使用最简单的阻塞式IO来做socket通信。总体设计计算节点和存储节点分工, 关键就是索引维护在哪?经过思考, 决定索引维护在计算节点。 索引的内容是: key -> <file_id, pos>, 索引在计算节点的内存中维护为hash表。因为build索引大概需要400ms,写入再读取index比重新构建一遍index时间还长, 所以索引不需要持久化, 这其实是一个反直觉的决定。 计算节点在发送第一个读请求之前,会从存储节点把所有写入的key和pos发送过来, 然后由计算节点构造索引。存储节点起16个线程,listen在16个不同的端口。计算节点也会有16个线程, 每个计算节点的线程只会连接一个存储节点的端口,从而和一个存储节点线程通信。写入请求:写入过程不同的线程完全独立,每个线程负责一部分数据。请求发到存储节点后由接受请求的线程写入。读取过程:读取过程每个存储线程会读取任意一个线程写入的数据。由计算节点指定要读取那个分区的数据。也即写请求发到哪个存储线程,就由那个存储线程写入,每个存储线程实际上就对应了一个数据分区。 读请求发到一个存储线程,它可能要跨线程读取其它分区的数据。存储文件存储节点把数据分为16个partition,每个partition由一个线程负责写入。 每个partition共三个文件: 之所以有三个文件,是因为首先key和value要分开存储,这就要用两个文件;其次为了优化写,额外引入了文件做写入buffer。文件命名规则如下, 以第一个分区为例:00.k: 保存写入的key。 fallocate 4M * 8B, mmap到内存。00.v:      保存写入的value。fallocate 4M *      4K, DIO读写。00.b:      用作写入buffer。fallocate 16K      buffer + 4K 元数据,mmap到内存。写入原子性先写key和value,再更新key count。 key count改成功,则写成功;否则写失败,下次重启进程当作这次写没发生过。换句话说,key count改成功实际上表示这次写入commit成功。key count记录在00.k的第一个8字节, 如前所诉, 00.k是mmap到内存的,所以更新key count是没有什么代价的。value先记录到写buffer里,然后批量刷到00.v文件。key file内容如下:key countk1k2k3k4k5k6……value file内容如下:value countv1v2v3v4v5v6……buffer file内容如下:b0b1b2b3…………b15flushed poswrite buffer也是mmap到内存的, 前64K记录数据。 紧跟64K的8个字节记录flushed pos。 因为mmap必须以page为单位,所以实际内存占用64K + 4K。 它实际上是一个mmap持久化的ring buffer。 Ring Buffer的元数据包括filled pos和flushed pos。 filled pos由key count可以算出来, 所以不需要单独再记。build indexbuild index实际上是构造一个key -> offset的hash表。 要在400ms内完成build index,难点是如何并行:基本思路是把key做partition,每个partition内独立构造hash。这本质上是一个MapReduce的过程。map阶段: 并行划分range, 16个线程,每个线程负责处理一个key文件。reduce阶段: 并行构造hash, 16个线程,每个线程处理一个range。线程同步: 第二阶段开始之前要等第一全部完成,也即两个Stage之间是个Barrier, 这个同步过程和map reduce的shuffle是类似的。读取cache的实现cache是value的镜像,value文件分成了16个,cache也对应分成16组。 因为顺序和热点访问模式对cache都很友好,cache不需要特别大,每一组cache大小为64M。为了应对热点读,cache的最小单元设为16M,借用CPU cache的术语,我把它叫cache line,这是一个很大的值。 cache的内存因为是定长的,所以通过mmap一次申请好,不需要动态分配。 cache的索引本质是一个hash,但是不用解决冲突, 用file offset直接计算得到索引的下标。对16组cache中的每一组来说,有4个cache line, 每个cache line有三种状态,用一个uint32_t表示:Invalid: UINT32_MAXLocked:      UINT32_MAX - 1Valid:      file_offset »12cache line的状态被叫做indexStat,它被单独记在另外一个小数组里。读取不用加锁,使用double check即可:读取之前检查IndexStat有效,并且offset和我要读取的内容匹配。拷贝cache line的内容到私有buffer。拷贝完cache line的内容后再检查IndexStat是否发生过变化, 如果indexStat没有变化过,则说明我们拷贝的cache      line内容是对的。为了让上述double check生效, 更新cache的线程需要在写入之前把indexStat改为Locked,更新完成后再把indexStat改为有效值。关于网络通信关于网络框架还有一个问题,就是是否有必要用UDP,犹豫之下,觉得UDP还是会更复杂,稳妥起见,选择了TCP。后来得知在测试环境里UDP是不同的,也就释然了。但是TCP看起来开箱即用,用起来却暗藏机关,至少要调整以下三个参数:tcp_nodelaytcp_quickacksend/recv      buf最后为了规避TCP的流控,我们要避免另外两个坑:避免发送太快,超过交换机的队列长度,从而导致丢包,因为TCP的工作方式就是,只要你给它数据,它就会不停的加大,发送窗口,直到发生丢包,然后触发限流。要避免这种情况,本质是要限制TCP的连接数, 因为只要连接数限制住了,发送窗口总长度也就限制住了。避免TCP”冷却”之后重新进入慢启动状态,如果有root权限,是可以通过sysctl关闭slow start的,但是我们没法控制系统参数,这就要求我们一个连接最好要不停的发包,不要停下来。有人定义了packet格式,而我遵循一切从简的原则,没有实现通常RPC要有的功能:没有定义pcode,因为整个通信过程就只有读和写两种request,每一个socket只用来发送一种类型的request,如果要发送不同的request,只需要切换socket即可。没有实现序列化,因为request很简单,只需要把结构体直接发送到socket即可。细节决定性能细节也就是很多关于性能的小点:首先关于文件IO: 同步IO比异步IO延迟要小; open的时候加入O_NOATIME避免读取操作也修改元数据其次关于内存分配: 分配完内后提前触发page fault至少可以让性能更稳定,如果不能提高性能的话;      分配内存可以尝试hugepage, 失败之后再使用4K page; mmap的内存可以不用清零最好关于CPU, 绑核,通用可以让延迟更稳定, 理论上对性能是有提升的。正确性测试如前所属,我基本上是一切从简,但即使如此,提交了好多次,都通不过正确性测试。为了排查问题,专门写了一个随机的验证程序,这个比官方的测试强度高多了。确实发现了一个readv之后更新iovec的bug,还有一处cache的bug。 事后看来,在这个测试上花的时间非常值得,否则我可能到最后都没有成绩。总结这次比赛感觉有点像马拉松,听完大家的分享,感觉每个人的时间都比较有限,大家都是争分夺秒,每个人都有一些优化没来得及试。本人最大的一个遗憾是读预取没有实现。另外一个是写的过程中网络和IO并行化做的不好。最后听了第一名的方案,深有体会,要想拿到最好的成绩,需要把写和读分开考虑,同时把网络传输和本地IO分开考虑。单纯从工程上说,我的代码有一些特有的风格, 简单来说就是总爱重复造轮子,不愿意引入依赖:没有用pthread mutex/cond,全部用atomic      ops + futex没有spin,等待的地方都有睡眠和唤醒机制另外,本人虽然工作中用C++, 但是我情愿用plain old C, 所以我很少用高级C++特性。如果未来还做这种系统实现的比赛,希望有可以利用RDMA的和NVM的比赛, 毕竟,新硬件总是有更多的可能。当然比赛的设置要考虑引入更多自由度,这样会更加有趣。
  • [已解决问题归档] CMS安装完成后,SUM进程启动失败
    【问题来源】星网【功能模块】AICC8.15【操作步骤&问题现象】安装完成CC-CMS后,CMSAPP和SIA进程启动正常,但是SUM启动失败,catalina.out日志截图如下【截图信息】【日志信息】(可选,上传日志内容或者附件)
  • [技术干货] 关于杀死Chrome进程后的“恢复页面弹窗”的永久关闭
    相信大家都有遇见,每当我们杀死Chrome进程后,再次打开Chrome,就会在右上角有一个恢复页面的弹窗,很烦人,有时候还会影响到流程的进行。不用担心,接下来就告诉大家如何永久关闭这个“恢复”弹窗。1.首先我们找到自己磁盘中的appdata目录(一般这个都在C盘User目录下,觉得不太好找的可以试试在路径上直接输入%appdata%,回车,再返回这个目录的上一级就可以了)2.然后我们找到后面的地址  C:\Users\+\AppData\Local\Google\Chrome\User Data\Default 在这个目录下找到Preferences文件3.我们需要右键选择用记事本打开Preferences文件,在里面直接快捷键CTRL+F 去查找exit_type,将"exit_type":"crashed" 改为"Normal"4.然后我们只需要保存关闭Preferences文件,再右键打开Preferences文件属性,将只读这个选项勾上,保存。即可享受永不弹出“恢复页面”的Chrome浏览器了!
  • [知识分享] 【数据库系列】GaussDB(DWS)中共享消息队列实现的三大功能
    >摘要:本文将详细介绍GaussDB(DWS)中共享消息队列的实现。本文分享自华为云社区[《GaussDB(DWS)CBB组件之共享消息队列介绍》](https://bbs.huaweicloud.com/blogs/317028?utm_source=csdn&utm_medium=bbs-ex&utm_campaign=ei&utm_content=content),作者:疯狂朔朔。 ## 1)共享消息队列是什么? 在前文中,我们讲解了SysCache的实现原理,GaussDB(DWS)通过SysCache缓存表元数据,以加速查询,然而在并发查询过程中,不可避免地会出现需要同步元数据的情况,举个简单例子,假设存在以下语句执行流程: 1. Create table abc(会话1) 2. Select * from abc(会话1) 3. Drop table abc(会话2) 4. Select * from abc(会话1) 在会话1中,会连续两次执行Select表操作(b和d),在b语句执行后,会话1将对abc的元数据进行缓存,缓存到SysCache中,以备后续使用。然而,在c语句执行后,需要对会话1中的元数据进行失效,否则,会话1将在执行d语句过程中发生错误,读取已删除的数据。 那么,会话2如何“通知”会话1失效哪些数据呢?答案是共享消息队列。 ## 2)共享消息队列存储结构 如图所示,为共享消息队列数据结构 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/24/105037oggabkl0enmuomrs.png) 图示中主要包括两部分,下面部分为ThreadLocal结构,主要记录的是每个线程内部的数据,线程间数据是独立的,无法互相访问,上面部分为共享消息队列中的数据,共享消息队列存在于共享内存中,可同时被多个线程访问,共享消息队列的访问场景是典型的一写多读场景。 在共享消息队列中,核心变量有三个,nextMsgNum、minMsgNum、MaxMsgNum,其中nextMsgNum记录了每个线程消息读取到的位置,minMsgNum记录了共享消息队列中最早消息的位置,maxMsgNum记录了共享消息队列中最新消息的位置,对于每个线程而言,需要定期(在表加锁/事务开始/收到信号时触发)从共享消息队列中读取失效消息,利用失效消息(共享消息队列中的每个消息)更新线程内部数据,同时,若线程内部产生失效消息(通常DDL语句在事务提交时产生大量失效消息),则需要向共享消息队列中插入失效数据,供其他线程读取。另外,还有两个参数,hasMessages、resetState,其中hasMessages用于标记对应线程是否存在未读取的失效消息,resetState用于标记对应线程是否需要失效全部消息。 NOTE:失效数据有哪些?失效消息一共有六类(源自PG),有兴趣的同学可以研读PG源码,在此处我们不再展开,仅需知道线程需要从共享消息队列中读取/插入消息,以实现数据同步。 ## 3)共享消息队列接口实现 共享消息队列本质上对于外部接口只需要提供三个功能:读取共享消息队列中消息、向共享消息队列中写入消息、清理共享消息队列。 ### 3.1)读取共享消息队列 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/24/105103kedkmjdimc9fbk4z.png) 如图所示,为失效消息读取过程。在线程同步失效消息过程中,有几个关键点: - 若共享内存中线程对应的hasMessage为True,则表示有失效消息需要读取,否则直接返回,无新的失效消息。 - 读取失效消息过程中,需要持有读共享锁,以保证读取的消息不会被清理掉。 - 若读取失效消息过程中,发现resetState被置为True,说明该线程已经无法使用共享消息队列中的消息进行追增,需要对缓存进行全失效。缓存全失效相当于追增全部数据,需要将nextMsgNum置为maxMsgNum。缓存全失效将严重降低SQL执行性能,尽量减少缓存全失效的发生频率。 - 在追增数据过程中,会推进线程自身的nextMsgNum,以标记数据追增位置。 NOTE:在读取共享消息队列过程中,每个线程推进自己的nextMsgNum位置,以方便记录数据追增情况。 ### 3.2)向共享消息队列中写入消息 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/24/105136bjshf3gdxrosxe28.png) 如图所示,为失效消息写入过程。有以下几个关键点: - 写入失效消息过程,需要调用清理共享消息队列接口,以保证有足够多的空位写入失效消息。 - 在写入失效消息过程中,会更新maxMsgNum。 - 写入失效消息完毕以后,需要将其他线程的hasMessage标记为True。 - 写入失效消息过程,需要持排他写锁,阻塞其他线程写操作,但不阻塞读。 NOTE:写入失效消息过程实际上是推进maxMsgNum的过程,同时告知其他线程有新的失效消息需要读取。 ### 3.3)清理共享消息队列 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/24/10520213zlw4xpfruowu9h.png) 如图所示,清理共享消息队列过程中,有以下几个关键点: - 清理共享消息队列需要持有排他读锁和排他写锁,阻塞读过程。其主要原因是,清理共享消息队列会推进minMsgNum,若不持读锁,可能导致nextMsgNum读取过期数据。 - 清理共享消息队列会推进minMsgNum。 - 清理共享消息队列过程,会将所有没有及时追增失效消息的线程执行全失效。 - 在清理共享消息队列最后步骤,会对距离minMsgNum最近的线程,发送追增信号,以确保不会频繁发生全失效。该步骤主要考虑的情况是,若线程长时间处于idle状态,需要外部信号触发其及时追增消息。 NOTE:清理共享消息队列过程,实际上是推进minMsgNum的过程,同时对所有没有及时追增失效消息的线程执行全失效。 根据以上共享消息队列接口可知,读取共享消息队列主要负责推进各个线程自身的nextMsgNum;写入失效消息主要负责推进maxMsgNum;清理共享消息队列主要负责推进minMsgNum。通过共享消息队列,可有效实现各个线程之间的数据同步。
  • [知识分享] 【微服务系列】认识容器,我们从它的历史开始聊起
    >摘要:Docker为什么火,靠的就是Docker镜像。他打包了应用程序的所有依赖,彻底解决了环境的一致性问题,重新定义了软件的交付方式,提高了生产效率。本文分享自华为云社区[《认识容器,我们从它的历史开始聊起》](https://bbs.huaweicloud.com/blogs/285728?utm_source=csdn&utm_medium=bbs-ex&utm_campaign=other&utm_content=content),作者:技术火炬手。 关于容器的历史、发展以及技术本质,在互联网上已经有非常多的文章了。这里旨在结合自身的工作经验和理解,通过一系列的文章,讲清楚这项技术。 # 容器的历史和发展 ### 1、前世 讲到容器,就不得不提LXC(Linux Container),他是Docker的前生,或者说Docker是LXC的使用者。完整的LXC能力在2008年合入Linux主线,所以容器的概念在2008年就基本定型了,并不是后面Docker造出来的。关于LXC的介绍很多,大体都会说“LXC是Linux内核提供的容器技术,能提供轻量级的虚拟化能力,能隔离进程和资源”,但总结起来,无外乎就两大知识点Cgroups(Linux Control Group)和Linux Namespace。搞清楚他俩,容器技术就基本掌握了。 - Cgroups:重点在“限制”。限制资源的使用,包括CPU、内存、磁盘的使用,体现出对资源的管理能力。 - Namespace:重点在“隔离”。隔离进程看到的Linux视图。说大白话就是,容器和容器之间不要相互影响,容器和宿主机之间不要相互影响。 ### 2、少年期起步艰难 2009年,Cloud Foundry基于LXC实现了对容器的操作,该项目取名为Warden。2010年,dotCloud公司同样基于LXC技术,使用Go语言实现了一款容器引擎,也就是现在的Docker。那时,dotCloud公司还是个小公司,出生卑微的Docker没什么热度,活得相当艰难。 ### 3、 成长为巨无霸 2013年,dotCloud公司决定将Docker开源。开源后,项目突然就火了。从大的说,火的原因就是Docker的这句口号“Build once,Run AnyWhere”。呵呵,是不是似曾相识?对的,和Java的Write Once,Run AnyWhere一个道理。对于一个程序员来说,程序写完后打包成镜像就可以随处部署和运行,开发、测试和生产环境完全一致,这是多么大一个诱惑。程序员再也不用去定位因环境差异导致的各种坑爹问题。 Docker开源项目的异常火爆,直接驱动dotCloud公司在2013年更名为Docker公司。Docker也快速成长,干掉了CoreOS公司的rkt容器和Google的lmctfy容器,直接变成了容器的事实标准。也就有了后来人一提到容器就认为是Docker。 总结起来,Docker为什么火,靠的就是Docker镜像。他打包了应用程序的所有依赖,彻底解决了环境的一致性问题,重新定义了软件的交付方式,提高了生产效率。 ### 4、 被列强蚕食 Docker在容器领域快速成长,野心自然也变大了。2014年推出了容器云产品Swarm(K8s的同类产品),想扩张事业版图。同时Docker在开源社区拥有绝对话语权,相当强势。这种走自己的路,让别人无路可走的行为,让容器领域的其他大厂玩家很是不爽,为了不让Docker一家独大,决定要干他。 2015年6月,在Google、Redhat等大厂的“运作”下,Linux基金会成立了OCI(Open Container Initiative)组织,旨在围绕容器格式和运行时制定一个开放的工业化标准,也就是我们常说的OCI标准。同时,Docker公司将Libcontainer模块捐给CNCF社区,作为OCI标准的实现,这就是现在的RunC项目。说白了,就是现在这块儿有个标准了,大家一起玩儿,不被某个特定项目的绑定。 讲到Docker,就得说说Google家的Kubernetes,他作为容器云平台的事实标准,如今已被广泛使用,俨然已成为大厂标配。Kubernetes原生支持Docker,让Docker的市场占有率一直居高不下。如图是2019年容器运行时的市场占有率。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/24/095520zlfv6d2vhscjg9c0.png) 但在2020年,Kubernetes突然宣布在1.20版本以后,也就是2021年以后,不再支持Docker作为默认的容器运行时,将在代码主干中去除dockershim。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/24/095534m3njj6fcpkafodbq.png) 如图所示,K8s自身定义了标准的容器运行时接口CRI(Container Runtime Interface),目的是能对接任何实现了CRI接口的容器运行时。在初期,Docker是容器运行时不容置疑的王者,K8s便内置了对Docker的支持,通过dockershim来实现标准CRI接口到Docker接口的适配,以此获得更多的用户。随着开源的容器运行时Containerd(实现了CRI接口,同样由Docker捐给CNCF)的成熟,K8s不再维护dockershim,仅负责维护标准的CRI,解除与某特定容器运行时的绑定。当然,也不是K8s不支持Docker了,只是dockershim谁维护的问题。 随着K8s态度的变化,预计将会有越来越多的开发者选择直接与开源的Containerd对接,Docker公司和Docker开源项目(现已改名为moby)未来将会发生什么样的变化,谁也说不好。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/24/095544zkpq6jusz5fbqvq1.png) 讲到这里,不知道大家有没有注意到,Docker公司其实是捐献了Containerd和runC。这俩到底是啥东西。简单的说,runC是OCI标准的实现,也叫OCI运行时,是真正负责操作容器的。Containerd对外提供接口,管理、控制着runC。所以上面的图,真正应该长这样。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/24/0955522szerhigfhuhet3k.png) Docker公司是一个典型的小公司因一个爆款项目火起来的案例,不管是技术层面、公司经营层面以及如何跟大厂缠斗,不管是好的方面还是坏的方面,都值得我们去学习和了解其背后的故事。 # 什么是容器 按国际惯例,在介绍一个新概念的时候,都得从大家熟悉的东西说起。幸好容器这个概念还算好理解,喝水的杯子,洗脚的桶,养鱼的缸都是容器。容器技术里面的“容器”也是类似概念,只是装的东西不同罢了,他装的是应用软件本身以及软件运行起来需要的依赖。用鱼缸来类比,鱼缸这个容器里面装的应用软件就是鱼,装的依赖就是鱼食和水。这样大家就能理解docker的logo了。大海就是宿主机,docker就是那条鲸鱼,鲸鱼背上的集装箱就是容器,我们的应用程序就装在集装箱里面。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/24/095613p1cmktl4clepa9ku.png) 在讲容器的时候一定绕不开容器镜像,这里先简单的把容器镜像理解为是一个压缩包。压缩包里包含应用的可执行程序以及程序依赖的文件(例如:配置文件和需要调用的动态库等),接下来通过实际操作来看看容器到底是个啥。 ## 一、宿主机视角看容器: **1、首先,我们启动容器。** `docker run -d --name="aimar-1-container" euleros_arm:2.0SP8SPC306 /bin/sh -c "while true; do echo aimar-1-container; sleep 1; done"` 这是Docker的标准命令。意思是使用euleros_arm:2.0SP8SPC306镜像(镜像名:版本号)创建一个新的名字为"aimar-1-container"的容器,并在容器中执行shell命令:每秒打印一次“aimar-1-container”。 - **参数说明:** -d:使用后台运行模式启动容器,并返回容器ID。 --name:为容器指定一个名字。 docker run -d --name="aimar-1-container" euleros_arm:2.0SP8SPC306 /bin/sh -c "while true; do echo aimar-1-container; sleep 1; done" 207b7c0cbd811791f7006cd56e17033eb430ec656f05b6cd172c77cf45ad093c 从输出中,我们看到一串长字符207b7c0cbd811791f7006cd56e17033eb430ec656f05b6cd172c77cf45ad093c。他就是容器ID,能唯一标识一个容器。当然在使用的时候,不需要使用全id,直接使用缩写id即可(全id的前几位)。例如下图中,通过docker ps查询到的容器id为207b7c0cbd81 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/24/095752kjcmoypnog2itjl4.png) aimar-1-container容器启动成功后,我们在宿主机上使用ps进行查看。这时可以发现刚才启动的容器就是个进程,PID为12280。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/24/0958020useuirw8znsxwk6.png) 我们尝试着再启动2个容器,并再次在宿主机进行查看,你会发现又新增了2个进程,PID分别为20049和21097。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/24/095819nfq66zfu69mxdmsa.png) 所以,我们可以得到一个结论。**从宿主机的视角看,容器就是进程。** **2、接下来,我们进入这个容器。** `docker exec -it 207b7c0cbd81 /bin/bash` docker exec也是Docker的标准命令,用于进入某个容器。意思是进入容器id为207b7c0cbd81的容器,进入后执行/bin/bash命令,开启命令交互。 - **参数说明:** -it其实是-i和-t两个参数,意思是容器启动后,要分配一个输入/输出终端,方便我们跟容器进行交互,实现跟容器的“对话”能力。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/24/095908rggk8nthya49r13n.png) 从hostname从kwephispra09909变化为207b7c0cbd81,说明我们已经进入到容器里面了。在容器中,我们尝试着启动一个新的进程。 `[root@207b7c0cbd81 /]# /bin/sh -c "while true; do echo aimar-1-container-embed; sleep 1; done" &` ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/24/095928aov19uyf1npegag6.png) 再次回到宿主机进行ps查看,你会发现**不管是直接启动容器,还是在容器中启动新的进程,从宿主机的角度看,他们都是进程。** # 二、容器视角看容器: 前面我们已经进入容器里面,并启动了新的进程。但是我们并没有在容器里查看进程的情况。在容器中执行ps,会发现得到的结果和宿主机上执行ps的结果完全不一样。下图是容器中的执行结果。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/24/100019zgemqpa2wms49vjp.png) 在Container1容器中只能看见刚起启动的shell进程(container1和container1-embed),看不到宿主机上的其他进程,也看不到Container2和Container3里面的进程。这些进程像被关进了一个盒子里面,完全感知不到外界,甚至认为我们执行的container1是1号进程(1号进程也叫init进程,是系统中所有其他用户进程的祖先进程)。所以,**从容器的视角,容器觉得“我就是天,我就是地,欢迎来到我的世界”。** ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/24/100037isou8f0ckdd3a4ym.png) 但尴尬的是,在宿主机上,他们却是普通得不能再普通的进程。注意,相同的进程,在容器里看到的进程ID和在宿主机上看到的进程ID是不一样的。容器中的进程ID分别是1和1859,宿主机上对应的进程ID分别是12280和9775(见上图)。 # 三、总结 通过上面的实验,对容器的定义就需要再加上一个定语。**容器就是进程=>容器是与系统其他部分隔离开的进程。** 这个时候我们再看下图就更容易理解,容器是跑在宿主机OS(虚机容器的宿主机OS就是Guest OS)上的进程,容器间以及容器和宿主机间存在隔离性,例如:进程号的隔离。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/24/100055khovfhnu82plkzpn.png) 在容器内和宿主机上,同一个进程的进程ID不同。例如:Container1在容器内PID是1,在宿主机上是12280。那么该进程真正的PID是什么呢?当然是12280!那为什么会造成在容器内看到的PID是1呢,造成这种幻象的,正是Linux Namespace。 Linux Namespace是Linux内核用来隔离资源的方式。每个Namespace下的资源对于其他Namespace都是不透明,不可见的。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/24/100116dqlwj6k4swul8c4j.png) Namespace按隔离的资源进行分类: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/24/1001247spdfg2xwptwtqxa.png) 前面提到的容器内外,看到的进程ID不同,正是使用了PID Namespace。那么这个Namespace在哪呢?在Linux上一切皆文件。是的,这个Namespace就在文件里。在宿主机上的proc文件中(/proc/进程号/ns)变记录了某个进程对应的Namespace信息。如下图,其中的数字(例如:pid:[ 4026534312])则表示一个Namespace。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/24/1001348napuwckyqgilkdr.png) 对于Container1、Container2、Container3这3个容器,我们可以看到,他们的PID Namespace是不一样的。说明他们3个容器中的PID相互隔离,也就是说,这3个容器里面可以同时拥有PID号相同的进程,例如:都有PID=1的进程。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/24/100143mbezjyp3t6zddk19.png) 在一个命名空间中,那这俩进程就相互可见,只是PID与宿主机上看到的不同而已。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/24/100151grurjkq7cfkodbxq.png) 至此,我们可以对容器的定义再细化一层。**容器是与系统其他部分隔离开的进程=》容器是使用Linux Namespace实现与系统其他部分隔离开的进程。**
总条数:813 到第 页
上滑加载中