首页 文章 精选 留言 我的

精选列表

搜索[鸿蒙实践],共10000篇文章
优秀的个人博客,低调大师

PikiwiDB(Pika) 3.5 最佳实践

PikiwiDB(Pika) 是 360 技术中台中间件团队基于 RocksDB 开发的大容量类 Redis 存储系统,力求在完全兼容 Redis 协议、继承 Redis 便捷运维设计的前提下通过持久化存储方式解决 Redis 在大容量场景下主从同步代价高、恢复时间慢、单线程相对脆弱、内存成本高等问题。 我们根据 360 内部的 PikiwiDB(Pika) 使用经验及社区用户的问题反馈,整理了本文并在这里分享给大家。 之一 在微信群(群管理员微信号:PikiwiDB)中提问时,请主动带上版本号,可大幅度加快问题解决速度。 之二 PikiwiDB(Pika) 已在 2024 年 5 月更新至 3.5.4,但仍然有大量用户停留在 3.3.6 或3.3.2,我们建议使用 3.5.4 的最新版(预计本周内发布 v4.0.0),你会发现你遇到的很多问题都在我们的 bug 修复列表中。 之三 PikiwiDB(Pika) 的线程数量 thread-num 建议设置为 CPU core 数目的 80% 左右,如果是单机多实例的部署,每个 PikiwiDB(Pika) 实例的线程数量可以酌情降低,但不建议低于 CPU core 数的 1/2。 之四 PikiwiDB(Pika) 的工作线程池数量 thread-pool-size 建议和 CPU core 数目一致,如果是单机多实例的部署,每个 PikiwiDB(Pika) 实例的线程数量可以酌情降低,但不建议低于 1/2 CPU core 数。 之五 PikiwiDB(Pika) 的性能和 IO 性能息息相关,如果对耗时非常敏感,建议使用 NVMe SSD。另外,主从服务器的硬件规格应当尽量一致。 之六 在使用 PikiwiDB(Pika) 复合数据结构(hash,list,zset,zset)时,尽量确保每个 key 中的二级 key(或者成为 field)不要太多(不要超过 1 万个),在业务层或者代理层对大 key 符合数据结构进行拆分(类似于分库分表), 这样可以避免超大 key 带来很多潜在的性能风险。 之七 root-connection-num 参数非常有用,意为“允许通过 127.0.0.1 登录 PikiwiDB(Pika) 的连接数”,它不会被算进客户端最大连接数配置项 maxclients,因此在发生异常 maxclients 被用尽的场景中,管理员仍然可以登录 PikiwiDB(Pika) 所在服务器并通过 127.0.0.1 登入 PikiwiDB(Pika) 处理问题,可以认为是超级管理员通道。 之八 client kill 命令被加强了,如果你想一次性杀掉当前 PikiwiDB(Pika) 的所有客户端连接,只需要执行 client kill all 命令即可。注意,主从同步的网络连接不受影响。 之九 适当地调整 timeout 参数,PikiwiDB(Pika) 会主动断开不活跃时间超过 timeout 值的连接,避免连接数耗尽。由于网络连接会占用主机内存,因此合理的配置 timeout 参数也能够在一定程度上降低 PikiwiDB(Pika) 的内存使用量。 之十 PikiwiDB(Pika) 的内存占用主要集中在 SST 文件的 cache 和网络连接内存占用量,通常网络连接内存量会比 SST 的 cache 大,PikiwiDB(Pika) 目前已支持连接申请内存的动态调整与回收,因此连接占用的总内存大小是可以粗略估算的,如果你的 PikiwiDB(Pika) 内存占用远超预估(如大于 10GiB),那么可能为你当前使用的版本存在内存泄漏问题,尝试依次执行命令 client kill all 对连接内存进行强制回收,或者升级到最新版本。 之十一 非常不建议单副本运行 PikiwiDB(Pika),单副本的数据安全性无法保障,诸如 RocksDB Bug 或者资源不够(如:ERR IO error: While fdatasync: /data1/db/zsets/16566747.log: Cannot allocate memory)导致 RocksDB 存储数据被污染,此时无法全量恢复数据。 最简集群状态应为一主一从。 之十二 如果 PikiwiDB(Pika) 单副本运行(非主从集群),只在乎性能,且不在乎数据安全性(如缓存场景),可以考虑通过关闭 binlog(将 write-binlog 参数设置为 no)来提高写入性能。 之十三 PikiwiDB(Pika) v3.5.2 以及之后的版本提供了关闭 RocksDB WAL (DisableWAL true) 的命令,如果你的 PikiwiDB(Pika) 实例出现间断性的写性能阻塞的情况,你可以通过关闭 WAL 命令暂时关闭 WAL,这种方式有断电情况下数据丢失的风险,待性能恢复时,请及时再打开。对数据完整性要求不高时,建议关闭 WAL。 之十四 PikiwiDB(Pika) 的数据目录中有大量的 SST 文件,这些文件随着 PikiwiDB(Pika) 数据量的增加而增加,建议为 PikiwiDB(Pika) 配置一个较大的 open_file_limit ,以避免 fd 不够用,如果不希望 Pika 占用太多的文件描述符,可以通过适当增大单个 SST 的体积来降低 SST 的总数量,对应参数为 target-file-size-base。 之十五 不要修改 log 目录中的 write2file 文件和 manifest。write2file 记录了 binlog 文件列表等关键信息,而 manifest 则记录了 RocksDB 的 version 信息,二者关乎 PikiwiDB(Pika) 实例重启后的 binlog 续写及 slave 断点续传时的数据正确性。 之十六 自 PikiwiDB(Pika) v3.5.0 之后的版本摒弃了用 rsync 进程进行全量同步,PikiwiDB(Pika) 进程内部重新实现了一套新的全量同步机制(通过名称为 rsync 的线程传输)。PikiwiDB(Pika) 提供了 rsync 的总传输限速参数 throttle-bytes-per-second 和并发 rsync 线程数 max-rsync-parallel-num,throttle-bytes-per-second 参数的单位是 MiB,建议在千兆环境中该参数设置不应高于 45,而在万兆环境中不应高于 500,以避免 PikiwiDB(Pika) 在全量同步的时候将所在服务器网卡流量用尽而影响到 PikiwiDB(Pika) 服务客户端。 之十七 在 PikiwiDB(Pika) 中执行 “ key * ” 并不会造成 Pika 阻塞(PikiwiDB(Pika) 是多线程的),但在存在巨量 key 的场景下可能会造成临时占用巨量内存(这些内存用于该连接存放 key *的执行结果,会在 “ key * ”执行完毕后释放),因此使用 “ key * ” 一定要小心谨慎。 之十八 如果发现 PikiwiDB(Pika) 有数据但 info keyspace 的显示均为 0,这是因为 Pika 并没有像 Redis 那样对 key 的数量进行实时统计,PikiwiDB(Pika) 中 key 的统计需要人工触发,执行 info keyspace 1,注意执行 info keyspace 是不会触发统计的,没有带上最后的参数 1 将会仅仅展示上一次的统计结果,key 的统计是需要时间的,执行状态可以通过 info stats 中的 is_scaning_keyspace 进行查看,该项值为 yes 表明统计正在进行,为 no 时表明没有正在进行的统计/上一次统计已结束,在统计执行完毕前 info keyspace 不会更新,info keyspace 的数据是存放在内存里的,重启将清零。 之十九 不要在 PikiwiDB(Pika) 执行全量 compact 的时候触发 key 统计(info keyspace 1)或执行 keys *,否则会造成数据体积暂时膨胀直到 key 统计、keys *执行结束。 之二十 对存在大量过期数据的 PikiwiDB(Pika) 实例,compact-cron 配置项可以在固定时段(一般配置为低峰流量时间段)进行过期数据清理。自 PikiwiDB(Pika) v3.5.0 之后还提供了 auto_compact 配置型,启用后 PikiwiDB(Pika) 会自动周期性执行 compact。 异常的数据体积(大于估算值 10%以上),可以通过执行 compact 命令,在 compact 执行完毕后观察数据体积是否恢复正常。 请求耗时突然异常增大,可以通过执行 compact 命令,在 compact 执行完毕后观察请求耗时是否恢复正常。 之二十一 自 PikiwiDB(Pika) v3.5.0 之后可统计过期 key(可通过 info keyspace 1 来触发统计,通过 info keyspace 查看统计结果),统计结果中的 invaild_keys 的值为“已删除/过期但还未被物理删除的 key 的数量”,PikiwiDB(Pika) 会在后台逐步地对已删除/过期的 key 进行物理清理,由于这是一个后台行为,因此在存在大规模过期 key 的场景下这些 key 可能无法被及时清理,因此建议关注该值,若发现无效 key 数量过多可通过 compact 命令进行全面清理,这样能够将未物理清理的无效数据控制在一个较好的程度从而确保 Pika 的性能稳定,如果 PikiwiDB(Pika) 中存储的数据是规律性过期的,例如每个 key 的过期时间为 7 天,那么建议通过配置 compact-cron 参数来实现每天的定时自动进行全量 compact,compact 会占用一定的 IO 资源,因此如果磁盘 IO 压力过大,建议将其配置为业务低峰期执行,例如深夜。 之二十二 write2file 的角色相当于 binlog,建议 write2file 保留周期/数量不低于 48 小时,足够的 write2file 有利于 大数据集群的从库扩容、从库服务器关机维修、从库迁移 等工作,不会因为主库 write2file 过期而被迫全量重传。 之二十三 PikiwiDB(Pika) 的备份生成为快照式,通过硬链接存放在 dump 目录下,以日期为后缀,每天只生成一份,多次生成备份时新的备份会覆盖之前的旧文件。在生成备份快照的时,为了确保数据的一致性 PikiwiDB(Pika) 会暂时阻塞写入,阻塞时间与实际数据量相关,根据测试PikiwiDB(Pika) 生成 500GiB 备份快照仅需 50ms。在写入阻塞的过程中连接不会中断,但 client 会感觉到 “在那一瞬间请求耗时增加了一些”。由于PikiwiDB(Pika)Pika 的快照是 db 目录中 sst 文件的硬连接,因此最初这个目录是不会占用磁盘空间的。 但在 PikiwiDB(Pika) db 目录中的 SST 文件发生了合并、删除后,硬链接的旧文件并不删除,这会导致 PikiwiDB(Pika) 占用的磁盘空间超出预估,所以请根据实际的磁盘空间调整备份保留天数,避免备份太多而造成磁盘空间用尽。 之二十四 如果写入量巨大且磁盘性能不足以满足 RocksDB memtable 的及时刷盘需求,那么 RocksDB 很可能会进入写保护模式(write stall,写入将被全部阻塞),建议更换性能更好的存储系统来支撑,或者降低写入频率(例如将集中写数据的 2 小时拉长到 4 小时),也可适当加大 write-buffer-size 的值来提高 memtable 的总容量从而降低整个 memtable 被写满的可能。 之二十五 PikiwiDB(Pika) 对数据进行了压缩,默认压缩算法为 snappy,并允许改为 zlib,因此每一次数据的存入、读出都需要经过压缩、解压,这对 CPU 有一定的消耗,建议像使用 Redis 一样使用 PikiwiDB(Pika):在 PikiwiDB(Pika) 中关闭压缩,而在 client 中完成数据的压缩、解压,这样不仅能够降低数据体积,还能有效降低 Pikiw。注意关闭和开启压缩后,需要重启 PikiwiDB(Pika) 实例。 之二十六 读写分离很重要,PikiwiDB(Pika) 在常见的主从集群中由于写入是单点的(仅 master 支持写),因此写入性能是有极限的。可通过多个 slave 来共同支撑读流量,因此 PikiwiDB(Pika) 集群的读性能是随着 slave 数量的增加而增加的,所以对于读量很大的场景,建议在业务层代码加入读写分离策略,同时在 PikiwiDB(Pika) 层增加 slave 数量。 之二十七 全量 compact 的原理是逐步对 RocksDB 的每一层做数据合并、清理工作,在这个过程中会新增、删除大量的 SST 文件,因此在执行全量 compact 的时候可以发现数据体积先增大后减小并最终减小到一个稳定值(无效、重复数据合并、清理完毕仅剩有效数据),建议在执行 compact 前确保磁盘空余空间不低于 30%,以避免新增 SST 文件时将磁盘空间耗尽,另外 PikiwiDB(Pika) 支持对指定数据结构进行 compact,例如一个实例中已知 hashtable 结构的无效数据很少但 hashtable 结构数据量很大,set 结构数据量很大且无效数据很多,在这个例子中 hashtable 结构的 compaction(命令是 compact hash) 是没有必要的,你可以通过 compact set 实现只对 set 结构进行 compaction。 注意:在 PikiwiDB v4.0.0 版本之后,不再支持对特定类型的 compaction。因为 PikiwiDB v3.x 使用的存储引擎是 Blackwidow,每个数据类型使用一个 RocksDB,而 v4.0.0 的存储引擎升级为 Floyd,可以在单个 RocksDB 中存储所有类型的数据。 之二十八 PikiwiDB(Pika) 3.5.0 以后的版本支持通过 rate-limiter-bandwidth 配置项以限制磁盘 IO 速率,可以通过调整该配置参数来调整读写速度。在 v4.0.0 之前只支持写限速,在 v4.0.0 之后支持读写限速,可以通过调整配置参数中的 rate-limiter-mode 来设置限速模式。 之二十九 PikiwiDB(Pika) 和 Redis 一样支持慢日志功能,可通过 slowlog 命令查看。slowlog 的原始内容只存于内存中,内存空间有上限,且这个上限可配置,当然如果配置过大会造成 slowlog 占用太多内存。PikiwiDB(Pika) 也允许将 slowlog-write-errorlog 设置为 yes,以把慢日志记录到 pika.ERROR 日志中,用于追溯、分析。 之三十 PikiwiDB(Pika) v3.5.2 以后的版本支持冷热数据分离,并在 Pika 磁盘存储之上增加了内存缓存层(称之为 RedisCache),将用户访问的热数据放在缓存层,冷数据放在磁盘,可减少查询磁盘的次数,提升服务的读性能,不论 PikiwiDB(Pika) 使用的是主从复制模式还是集群模式,可以配置 cache-mode 为 1 ,并设置缓存的大小和个数,以提升读性能。如果实例内存较小,不足以支撑缓存层的资源耗费,你可以选择将 cache-mode 设置成为 0 将缓存层关闭掉。 之三十一 PikiwiDB(Pika) 3.5.3 以后的版本支持了 Redis ACL 功能,设置用户密码的方式发生了变化,ACL的认证方式和 Redis 保持一致,在 config 文件中按照 ACL 规则对 user 进行配置。PikiwiDB(Pika) 3.5.3 仍然兼容以前旧版本的认证方式。 之三十二 PikiwiDB(Pika) 3.5.3 以后的版本支持快、慢命令分离,有快、慢两个线程池,可以防止慢命令对快命令线程池阻塞的影响。可以通过 slow-cmd-list 配置项设置慢命令列表,通过设置 slow-cmd-thread-pool-size 设置慢命令线程池个数。 之三十三 欲知后事如何,且待微信群里分解。请添加 PikiwiDB 小助手【微信号: PikiwiDB】为好友,它会拉您加入官方微信群。

优秀的个人博客,低调大师

GaussDB表设计最佳实践

目录 如何选择存储模型 如何选择数据分布方式 如何选择分布列 其他最佳设计建议 使用局部聚簇 使用分区表 选择数据类型 如何选择存储模型 进行数据库设计时,表设计上的一些关键项将严重影响后续整库的查询性能。表设计对数据存储也有影响:好的表设计能够减少I/O操作及最小化内存使用,进而提升查询性能。 表的存储模型选择是表定义的第一步。客户业务属性是表的存储模型的决定性因素,依据下面表格选择适合当前业务的存储模型。 存储模型 适用场景 行存 点查询(返回记录少,基于索引的简单查询)。增删改比较多的场景。 列存 统计分析类查询 (group , join多的场景)。 如何选择数据分布方式 复制表(Replication)方式将表中的全量数据在集群的每一个DN实例上保留一份。主要适用于记录集较小的表。这种存储方式的优点是每个DN上都有该表的全量数据,在join操作中可以避免数据重分布操作,从而减小网络开销,同时减少了plan segment(每个plan segment都会起对应的线程);缺点是每个DN都保留了表的完整数据,造成数据的冗余。一般情况下只有较小的维度表才会定义为Replication表。 哈希(Hash)表将表中某一个或几个字段进行hash运算后,生成对应的hash值,根据DN实例与哈希值的映射关系获得该元组的目标存储位置。对于Hash分布表,在读/写数据时可以利用各个节点的IO资源,大大提升表的读/写速度。一般情况下大表定义为Hash表。 范围(Range)和列表(List)分布是由用户自定义的分布策略,根据分布列的取值落入满足一定范围或者具体值的对应目标DN,这两种分布方式便于用户灵活地进行数据管理,但对用户本身的数据抽象能力有一定的要求。 策略 描述 适用场景 Hash 表数据通过hash方式散列到集群中的所有DN实例上。 数据量较大的事实表。 Replication 集群中每一个DN实例上都有一份全量表数据。 小表、维度表。 Range 表数据对指定列按照范围进行映射,分布到对应DN。 用户需要自定义分布规则的场景。 List 表数据对指定列按照具体值进行映射,分布到对应DN。 用户需要自定义分布规则的场景。 如图1所示,复制表如图中的表T1,哈希表如图中的表T2。 图1复制表和哈希表 如何选择分布列 Hash分布表的分布列选取至关重要,需要满足以下原则: 列值应比较离散,以便数据能够均匀分布到各个DN。例如,考虑选择表的主键为分布列,如在人员信息表中选择身份证号码为分布列。 在满足上述条件的情况下,考虑选择查询中的连接条件为分布列,以便Join任务能够下推到DN中执行,且减少DN之间的通信数据量。 对于Hash分表策略,如果分布列选择不当,可能导致数据倾斜,查询时出现部分DN的I/O短板,从而影响整体查询性能。因此在采用Hash分表策略之后需对表的数据进行数据倾斜性检查,以确保数据在各个DN上是均匀分布的。可以使用以下SQL检查数据倾斜性 select xc_node_id, count(1) from tablename group by xc_node_id order by xc_node_id desc; 其中xc_node_id对应DN,一般来说,不同DN的数据量相差5%以上即可视为倾斜,如果相差10%以上就必须要调整分布列。 GaussDB支持多分布列特性,可以更好地满足数据分布的均匀性要求。 Range/List分布表的分布列由用户根据实际需要进行选择。除了需选择合适的分布列,还需要注意分布规则对数据分布的影响。 其他最佳设计建议 使用局部聚簇 局部聚簇(Partial Cluster Key)是列存下的一种技术。这种技术可以通过min/max稀疏索引较快的实现基表扫描的filter过滤。Partial Cluster Key可以指定多列,但是一般不建议超过2列。Partial Cluster Key的选取原则: 受基表中的简单表达式约束。这种约束一般形如col op const,其中col为列名,op为操作符 =、>、>=、<=、<,const为常量值。 尽量采用选择度比较高(过滤掉更多数据)的简单表达式中的列。 尽量把选择度比较高的约束col放在Partial Cluster Key中的前面。 尽量把枚举类型的列放在Partial Cluster Key中的前面。 使用分区表 分区表是把逻辑上的一张表根据某种方案分成几张物理块进行存储。这张逻辑上的表称之为分区表,物理块称之为分区。分区表是一张逻辑表,不存储数据,数据实际是存储在分区上的。分区表和普通表相比具有以下优点: 改善查询性能:对分区对象的查询可以仅搜索自己关心的分区,提高检索效率。 增强可用性:如果分区表的某个分区出现故障,表在其他分区的数据仍然可用。 方便维护:如果分区表的某个分区出现故障,需要修复数据,只修复该分区即可。 GaussDB支持的分区表为范围分区表。 范围分区表:将数据基于范围映射到每一个分区。这个范围是由创建分区表时指定的分区键决定的。分区键经常采用日期,例如将销售数据按照月份进行分区。 选择数据类型 高效数据类型,主要包括以下三方面: 尽量使用执行效率比较高的数据类型 一般来说整型数据运算(包括=、>、<、≧、≦、≠等常规的比较运算,以及group by)的效率比字符串、浮点数要高。比如某客户场景中对列存表进行点查询,filter条件在一个numeric列上,执行时间为10+s;修改numeric为int类型之后,执行时间缩短为1.8s左右。 尽量使用短字段的数据类型 长度较短的数据类型不仅可以减小数据文件的大小,提升IO性能;同时也可以减小相关计算时的内存消耗,提升计算性能。比如对于整型数据,如果可以用smallint就尽量不用int,如果可以用int就尽量不用bigint。 使用一致的数据类型 表关联列尽量使用相同的数据类型。如果表关联列数据类型不同,数据库必须动态地转化为相同的数据类型进行比较,这种转换会带来一定的性能开销。

优秀的个人博客,低调大师

Function源码解析与实践

作者:陈昌浩 1 导读 if…else…在代码中经常使用,听说可以通过 Java 8 的 Function 接口来消灭 if…else…!Function 接口是什么?如果通过 Function 接口接口消灭 if…else…呢?让我们一起来探索一下吧。 2 Function 接口 Function 接口就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口,Function 接口可以被隐式转换为 lambda 表达式。可以通过 FunctionalInterface 注解来校验 Function 接口的正确性。Java 8 允许在接口中加入具体方法。接口中的具体方法有两种,default 方法和 static 方法。 @FunctionalInterfaceinterface TestFunctionService{ void addHttp(String url);} 那么就可以使用 Lambda 表达式来表示该接口的一个实现。 TestFunctionService testFunctionService = url -> System.out.println("http:" + url); 2.1 FunctionalInterface 2.1.1 源码 @Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)public @interface FunctionalInterface {} 2.1.2 说明 上图是 FunctionalInterface 的注解说明。通过上面的注解说明,可以知道 FunctionalInterface 是一个注解,用来说明一个接口是函数式接口。 函数式接口只有一个抽象方法。 可以有默认方法,因为默认方法有一个实现,所以不是抽象的。函数接口的实例可以用 lambda 表达式、方法引用或构造函数引用创建。 FunctionalInterface 会校验接口是否满足函数式接口: 类型必须是接口类型,不能是注释类型、枚举或类。 只能有一个抽象方法。 可以有多个默认方法和静态方法。 可以显示覆盖 java.lang.Object 中的抽象方法。 编译器会将满足函数式接口定义的任何接口视为函数式接口,而不管该接口声明中是否使用 FunctionalInterface 注解。 3 Function 接口主要分类 Function 接口主要分类: Function:Function 函数的表现形式为接收一个参数,并返回一个值。 Supplier:Supplier 的表现形式为不接受参数、只返回数据。 Consumer:Consumer 接收一个参数,没有返回值。 Runnable:Runnable 的表现形式为即没有参数也没有返回值。 3.1 Function Function 函数的表现形式为接收一个参数,并返回一个值。 3.1.1 源码 @FunctionalInterfacepublic interface Function<T, R> { R apply(T t); default <V> Function<V, R> compose(Function<? super V, ? extends T> before) { Objects.requireNonNull(before); return (V v) -> apply(before.apply(v)); } default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) { Objects.requireNonNull(after); return (T t) -> after.apply(apply(t)); } static <T> Function<T, T> identity() { return t -> t; }} 3.1.2 方法说明 apply:抽象方法。将此函数应用于给定的参数。参数 t 通过具体的实现返回 R。 compose:default 方法。返回一个复合函数,首先执行 fefore 函数应用于输入,然后将该函数应用于结果。如果任意一个函数的求值引发异常,则将其传递给组合函数的调用者。 andThen:default 方法。返回一个复合函数,该复合函数首先对其应用此函数它的输入,然后对结果应用 after 函数。如果任意一个函数的求值引发异常,则将其传递给组合函数的调用者。 identity:static 方法。返回一个始终返回其输入参数的函数。 3.1.3 方法举例 1)apply 测试代码: public String upString(String str){ Function<String, String> function1 = s -> s.toUpperCase(); return function1.apply(str);} public static void main(String[] args) { System.out.println(upString("hello!")); } 通过 apply 调用具体的实现。执行结果: 2)compose 测试代码: public static void main(String[] args) { Function<String, String> function1 = s -> s.toUpperCase(); Function<String, String> function2 = s -> "my name is "+s; String result = function1.compose(function2).apply("zhangSan"); System.out.println(result);} 执行结果 如结果所示:compose 先执行 function2 后执行 function1。 3)andThen 测试代码: public static void main(String[] args) { Function<String, String> function1 = s -> s.toUpperCase(); Function<String, String> function2 = s -> "my name is "+s; String result = function1.andThen(function2).apply("zhangSan"); System.out.println(result);} 执行结果: 如结果所示: andThen 先执行 function1 后执行 function2。 identity 测试代码: public static void main(String[] args) { Stream<String> stream = Stream.of("order", "good", "lab", "warehouse"); Map<String, Integer> map = stream.collect(Collectors.toMap(Function.identity(), String::length)); System.out.println(map);} 执行结果: 3.2 Supplier Supplier 的表现形式为不接受参数、只返回数据。 3.2.1 源码 @FunctionalInterfacepublic interface Supplier<T> { /** * Gets a result. * * @return a result */ T get();} 3.2.2 方法说明 get:抽象方法。通过实现返回 T。 3.2.3 方法举例 public class SupplierTest { SupplierTest(){ System.out.println(Math.random()); System.out.println(this.toString()); }} public static void main(String[] args) { Supplier<SupplierTest> sup = SupplierTest::new; System.out.println("调用一次"); sup.get(); System.out.println("调用二次"); sup.get();} 执行结果: 如结果所示:Supplier 建立时并没有创建新类,每次调用 get 返回的值不是同一个。 3.3 Consumer Consumer 接收一个参数,没有返回值。 3.3.1 源码 @FunctionalInterfacepublic interface Consumer<T> { void accept(T t); default Consumer<T> andThen(Consumer<? super T> after) { Objects.requireNonNull(after); return (T t) -> { accept(t); after.accept(t); }; }} 3.3.2 方法说明 accept:对给定参数 T 执行一些操作。 andThen:按顺序执行 Consumer -> after ,如果执行操作引发异常,该异常被传递给调用者。 3.3.3 方法举例 public static void main(String[] args) { Consumer<String> consumer = s -> System.out.println("consumer_"+s); Consumer<String> after = s -> System.out.println("after_"+s); consumer.accept("isReady"); System.out.println("========================"); consumer.andThen(after).accept("is coming");} 执行结果: 如结果所示:对同一个参数 T,通过 andThen 方法,先执行 consumer,再执行 fater。 3.4 Runnable Runnable:Runnable 的表现形式为即没有参数也没有返回值。 3.4.1 源码 @FunctionalInterfacepublic interface Runnable { public abstract void run();} 3.4.2 方法说明 run:抽象方法。run 方法实现具体的内容,需要将 Runnale 放入到 Thread 中,通过 Thread 类中的 start()方法启动线程,执行 run 中的内容。 3.4.3 方法举例 public class TestRun implements Runnable { @Override public void run() { System.out.println("TestRun is running!"); }} public static void main(String[] args) { Thread thread = new Thread(new TestRun()); thread.start(); } 执行结果: 如结果所示:当线程实行 start 方法时,执行 Runnable 的 run 方法中的内容。 4 Function 接口用法 Function 的主要用途是可以通过 lambda 表达式实现方法的内容。 4.1 差异处理 原代码: @Datapublic class User { /** * 姓名 */ private String name; /** * 年龄 */ private int age; /** * 组员 */ private List<User> parters;} public static void main(String[] args) { User user =new User(); if(user ==null ||user.getAge() <18 ){ throw new RuntimeException("未成年!"); }} 执行结果: 使用 Function 接口后的代码: @FunctionalInterfacepublic interface testFunctionInfe { /** * 输入异常信息 * @param message */ void showExceptionMessage(String message);} public static testFunctionInfe doException(boolean flag){ return (message -> { if (flag){ throw new RuntimeException(message); } }); } public static void main(String[] args) { User user =new User(); doException(user ==null ||user.getAge() <18).showExceptionMessage("未成年!");} 执行结果: 使用 function 接口前后都抛出了指定的异常信息。 4.2 处理 if…else… 原代码: public static void main(String[] args) { User user =new User(); if(user==null){ System.out.println("新增用户"); }else { System.out.println("更新用户"); }} 使用 Function 接口后的代码: public static void main(String[] args) { User user =new User(); Consumer trueConsumer = o -> { System.out.println("新增用户"); }; Consumer falseConsumer= o -> { System.out.println("更新用户"); }; trueOrFalseMethdo(user).showExceptionMessage(trueConsumer,falseConsumer);}public static testFunctionInfe trueOrFalseMethdo(User user){ return ((trueConsumer, falseConsumer) -> { if(user==null){ trueConsumer.accept(user); }else { falseConsumer.accept(user); } });}@FunctionalInterfacepublic interface testFunctionInfe { /** * 不同分处理不同的事情 * @param trueConsumer * @param falseConsumer */ void showExceptionMessage(Consumer trueConsumer,Consumer falseConsumer);} 执行结果: 4.3 处理多个 if 原代码: public static void main(String[] args) { String flag=""; if("A".equals(flag)){ System.out.println("我是A"); }else if ("B".equals(flag)) { System.out.println("我是B"); }else if ("C".equals(flag)) { System.out.println("我是C"); }else { System.out.println("没有对应的指令"); }} 使用 Function 接口后的代码: public static void main(String[] args) { String flag="B"; Map<String, Runnable> map =initFunctionMap(); trueOrFalseMethdo(map.get(flag)==null).showExceptionMessage(()->{ System.out.println("没有相应指令"); },map.get(flag));}public static Map<String, Runnable> initFunctionMap(){ Map<String,Runnable> result = Maps.newHashMap(); result.put("A",()->{System.out.println("我是A");}); result.put("B",()->{System.out.println("我是B");}); result.put("C",()->{System.out.println("我是C");}); return result;}public static testFunctionInfe trueOrFalseMethdo(boolean flag){ return ((runnable, falseConsumer) -> { if(flag){ runnable.run(); }else { falseConsumer.run(); } });} 执行结果: 5 总结 Function 函数式接口是 java 8 新加入的特性,可以和 lambda 表达式完美结合,是非常重要的特性,可以极大的简化代码。

优秀的个人博客,低调大师

StoneDB读写分离实践方案

在 StoneDB 1.0 版本中,InnoDB 引擎处理 OLTP 的事务型业务,Tianmu 引擎处理 OLAP 的分析型业务。因此,需要在主从复制环境的基础上做读写分离,所有的写操作和部分读操作走 InnoDB 引擎,所有的分析类查询走 Tianmu 引擎。读写分离方案既可以使用第三方中间件,也可以在业务前端实现。本文介绍的是较为常用的中间件 ProxySQL。 服务器配置说明 IP Memory CPU OS version 192.168.30.40 8G 8C CentOS Linux release 7.9 192.168.30.41 8G 8C CentOS Linux release 7.9 192.168.30.42 8G 8C CentOS Linux release 7.9 192.168.30.46 16G 16C CentOS Linux release 7.9 注:主从环境中的各个服务器的配置一般情况下建议是一致的,但由于 StoneDB 不管重放 binlog,还是用于 OLAP 场景的查询,都是较消耗系统资源的,建议 StoneDB 配置略高于 MySQL。 主从环境说明 IP DATABASE ROLE DB version 192.168.30.40 MySQL master MySQL 5.7 192.168.30.41 / ProxySQL / 192.168.30.42 MySQL slave MySQL 5.7 192.168.30.46 StoneDB slave StoneDB 5.7 注:MySQL 与 StoneDB 的版本建议保持一致。 架构图说明 推荐采用一主两从的架构,上层的 ProxySQL 用于读写分离: 1)master(192.168.30.40)使用 InnoDB 引擎,可读写,提供 OLTP 场景的读写业务; 2)slave1(192.168.30.42)使用 InnoDB 引擎,只读,同时作为 standby,当 master 发生宕机时,可切换至 slave1,保证业务正常运行; 3)slave2(192.168.30.46)使用 Tianmu 引擎,只读,提供 OLAP 场景的读业务。 1、操作系统环境检查 操作系统环境检查的步骤在四个节点均需要执行。 1.1 关闭防火墙 # systemctl stop firewalld # systemctl disable firewalld 1.2 关闭SELINUX # vim /etc/selinux/config SELINUX = disabled 1.3 设置Swap分区 修改vm.swappiness的值为1,表示尽量不使用Swap。 # vi /etc/sysctl.conf vm.swappiness = 1 1.4 修改操作系统的限制 # ulimit -a core file size (blocks, -c) 0 data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 1031433 max locked memory (kbytes, -l) 64 max memory size (kbytes, -m) unlimited open files (-n) 65535 pipe size (512 bytes, -p) 8 POSIX message queues (bytes, -q) 819200 real-time priority (-r) 0 stack size (kbytes, -s) 10240 cpu time (seconds, -t) unlimited max user processes (-u) 1024 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited 修改操作系统的软硬限制 # vim /etc/security/limits.conf * soft nofile 65535 * hard nofile 65535 mysql soft nproc 1028056 mysql hard nproc 1028056 1.5 创建用户 # groupadd mysql # useradd -g mysql mysql # passwd mysql ProxySQL 节点无需创建,以上步骤执行完之后,重启操作系统。 2、部署MySQL 在 master 节点和 slave1 节点安装 MySQL。 2.1 下载安装包 https://downloads.mysql.com/archives/community/ 从官网下载 MySQL 5.7 的安装包。 2.2 卸载mariadb # rpm -qa|grep mariadb mariadb-5.5.56-2.el7.x86_64 mariadb-server-5.5.56-2.el7.x86_64 mariadb-libs-5.5.56-2.el7.x86_64 # yum remove mariadb* # rpm -qa|grep mariadb 2.3 上传tar包并解压 # tar -zxvf mysql-5.7.36-linux-glibc2.12-x86_64.tar.gz -C /usr/local/ # cd /usr/local/ # mv mysql-5.7.36-linux-glibc2.12-x86_64 mysql 2.4 创建目录 # mkdir -p /mysql/data/ # mkdir -p /mysql/log # chown -R mysql:mysql /mysql/ 2.5 配置参数文件 my.cnf master # vim /etc/my.cnf [client] port = 3306 socket = /mysql/data/mysql.sock [mysqld] port = 3306 basedir = /usr/local/mysql datadir = /mysql/data socket = /mysql/data/mysql.sock pid_file = /mysql/data/mysqld.pid log_error = /mysql/log/mysqld.log log_bin = /mysql/log/mybinlog server_id = 40 character_set_server = utf8mb4 collation_server = utf8mb4_general_ci max_connections = 1000 binlog_format = row default_storage_engine = innodb read_only=0 innodb_buffer_pool_size = 4096000000 innodb_log_file_size = 1024000000 innodb_log_files_in_group = 3 innodb_io_capacity = 4000 innodb_io_capacity_max = 8000 #开启GTID模式 gtid_mode = on enforce_gtid_consistency = 1 #并行复制 binlog_transaction_dependency_tracking = WRITESET transaction_write_set_extraction = XXHASH64 slave1 # vim /etc/my.cnf [client] port = 3306 socket = /mysql/data/mysql.sock [mysqld] port = 3306 basedir = /usr/local/mysql datadir = /mysql/data socket = /mysql/data/mysql.sock pid_file = /mysql/data/mysqld.pid log_error = /mysql/log/mysqld.log log_bin = /mysql/log/mybinlog server_id = 42 character_set_server = utf8mb4 collation_server = utf8mb4_general_ci max_connections = 1000 binlog_format = row default_storage_engine = innodb read_only=1 innodb_buffer_pool_size = 4096000000 innodb_log_file_size = 1024000000 innodb_log_files_in_group = 3 innodb_io_capacity = 4000 innodb_io_capacity_max = 8000 #开启GTID模式 gtid_mode = on enforce_gtid_consistency = 1 #并行复制 slave_parallel_type = LOGICAL_CLOCK slave_parallel_workers = 4 2.6 初始化实例 /usr/local/mysql/bin/mysqld --defaults-file=/etc/my.cnf --initialize --user=mysql 2.7 启动实例 /usr/local/mysql/bin/mysqld_safe --defaults-file=/etc/my.cnf --user=mysql & 注:管理员用户的临时密码在 mysqld.log 中,第一次登陆后需要修改管理员用户的密码。 3、部署StoneDB 3.1 下载安装包 https://stonedb.io/zh/docs/download/ 从官网下载 StoneDB 5.7 的安装包。 3.2 上传tar包并解压 # cd / # tar -zxvf stonedb-ce-5.7-v1.0.0.el7.x86_64.tar.gz 用户可根据安装规范将安装包上传至服务器,解压出来的目录是 stonedb57,示例中的安装路径是 /stonedb57。 3.3 检查依赖文件 # cd /stonedb57/install/bin # ldd mysqld # ldd mysql 如果检查返回有关键字"not found",说明缺少文件,需要安装对应的依赖包。例如: libsnappy.so.1 => not found 在 Ubuntu 上使用命令 "sudo apt search libsnappy" 检查,说明需要安装 libsnappy-dev。在 RedHat 或者 CentOS 上使用命令 "yum search all snappy" 检查,说明需要安装 snappy-devel、snappy。 3.4 创建目录 mkdir -p /stonedb57/install/data mkdir -p /stonedb57/install/binlog mkdir -p /stonedb57/install/log mkdir -p /stonedb57/install/tmp mkdir -p /stonedb57/install/redolog mkdir -p /stonedb57/install/undolog chown -R mysql:mysql /stonedb57 3.5 配置参数文件 my.cnf # vim /stonedb57/install/my.cnf [client] port = 3306 socket = /stonedb57/install/tmp/mysql.sock [mysqld] port = 3306 basedir = /stonedb57/install/ datadir = /stonedb57/install/data socket = /stonedb57/install/tmp/mysql.sock pid_file = /stonedb57/install/data/mysqld.pid log_error = /stonedb57/install/log/mysqld.log log_bin = /stonedb57/install/binlog/binlog server_id = 46 character_set_server = utf8mb4 collation_server = utf8mb4_general_ci max_connections = 1000 binlog_format = row default_storage_engine = tianmu read_only=1 innodb_buffer_pool_size = 2048000000 innodb_log_file_size = 1024000000 innodb_log_files_in_group = 3 innodb_io_capacity = 4000 innodb_io_capacity_max = 8000 innodb_log_group_home_dir = /stonedb57/install/redolog/ innodb_undo_directory = /stonedb57/install/undolog/ innodb_undo_log_truncate = 1 innodb_undo_tablespaces = 3 innodb_undo_logs = 128 #开启GTID模式 gtid_mode = on enforce_gtid_consistency = 1 #并行复制 slave_parallel_type = LOGICAL_CLOCK slave_parallel_workers = 8 3.6 初始化实例 /stonedb57/install/bin/mysqld --defaults-file=/stonedb57/install/my.cnf --initialize --user=mysql 3.7 启动实例 /stonedb57/install/bin/mysqld_safe --defaults-file=/stonedb57/install/my.cnf --user=mysql & 注:管理员用户的临时密码在 mysqld.log 中,第一次登陆后需要修改管理员用户的密码。 4、配置主从 4.1 创建复制用户 create user 'repl'@'%' identified by 'mysql123'; grant replication slave on *.* to 'repl'@'%'; 4.2 备份主库 /usr/local/mysql/bin/mysqldump -uroot -pmysql123 --single-transaction --set-gtid-purged=on -B aa > /tmp/aa.sql 4.3 传输备份文件 scp /tmp/aa.sql root@192.168.30.42:/tmp scp /tmp/aa.sql root@192.168.30.43:/tmp 注:如果数据较大,建议使用 mydumper. 4.4 slave1节点 /usr/local/mysql/bin/mysql -uroot -pmysql123 -S /mysqldb/data/mysql.sock source /tmp/aa.sql 注:恢复前需要确保 gtid_executed 为空。 4.5 slave2节点 在恢复前,需要修改存储引擎,注释锁表语句。 sed -i 's/UNLOCK TABLES/-- UNLOCK TABLES/g' /tmp/aa.sql sed -i 's/LOCK TABLES `/-- LOCK TABLES `/g' /tmp/aa.sql sed -i 's/ENGINE=InnoDB/ENGINE=tianmu/g' /tmp/aa.sql /stonedb57/install/bin/mysql -uroot -pmysql123 -S /stonedb57/install/tmp/mysql.sock source /tmp/aa.sql 注:恢复前需要确保 gtid_executed 为空。 4.6 建立主从复制 slave1节点 CHANGE MASTER TO MASTER_HOST='192.168.30.40', MASTER_PORT=3306, MASTER_USER='repl', MASTER_PASSWORD='mysql123', MASTER_AUTO_POSITION = 1; start slave; show slave status\G slave2节点 CHANGE MASTER TO MASTER_HOST='192.168.30.40', MASTER_PORT=3306, MASTER_USER='repl', MASTER_PASSWORD='mysql123', MASTER_AUTO_POSITION = 1; start slave; show slave status\G 5、配置ProxySQL 5.1 安装ProxySQL # mkdir -p /home/ProxySQL # cd /home/ProxySQL # yum install proxysql-2.2.0-1-centos7.x86_64.rpm # rpm -qa|grep proxysql # rpm -ql proxysql 注:/etc/proxysql.cnf 为 ProxySQL 的配置文件。 5.2 启动ProxySQL # systemctl start proxysql # netstat -lntp|grep proxysql tcp 0 0 0.0.0.0:6032 0.0.0.0:* LISTEN 17957/proxysql tcp 0 0 0.0.0.0:6033 0.0.0.0:* LISTEN 17957/proxysql 注:6032是管理端口,6033是服务端口。 5.3 管理员登录ProxySQL # mysql -uadmin -padmin -h127.0.0.1 -P6032 --prompt='ProxySQL>' mysql: [Warning] Using a password on the command line interface can be insecure. Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 1 Server version: 5.5.30 (ProxySQL Admin Module) Copyright (c) 2000, 2021, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. ProxySQL>show databases; +-----+---------------+-------------------------------------+ | seq | name | file | +-----+---------------+-------------------------------------+ | 0 | main | | | 2 | disk | /var/lib/proxysql/proxysql.db | | 3 | stats | | | 4 | monitor | | | 5 | stats_history | /var/lib/proxysql/proxysql_stats.db | +-----+---------------+-------------------------------------+ 5 rows in set (0.00 sec) 注:ProxySQL 的用户名和密码默认都是 admin,加参数 prompt 是为了便于区分环境。 5.4 创建ProxySQL所需用户 在 master 上创建 ProxySQL 的监控用户和对外访问用户,其中监控用户只需要有 replication client 权限即可。 ###监控用户 create user 'monitor'@'%' identified by 'MySQL_123'; grant replication client on *.* to 'monitor'@'%'; ###对外访问用户(用于连接ProxySQL) create user 'proxysql'@'%' identified by 'MySQL_123'; grant select,delete,update,insert on *.* to 'proxysql'@'%'; 5.5 配置ProxySQL主从分组信息 1)创建分组 表 mysql_replication_hostgroups 的字段 writer_hostgroup、reader_hostgroup 分别代表写组和读组,都要大于0且不能相同,该环境中定义写组为10,读组为20。 ProxySQL 会根据 read_only 的取值将 server 进行分组,read_only=0为 master,被分到编号为10的写组,read_only=1为 slave,被分到编号为20的读组。 ###创建分组 insert into mysql_replication_hostgroups(writer_hostgroup,reader_hostgroup,comment) values(10,20,'proxy'); load mysql servers to runtime; save mysql servers to disk; ###查看三层配置系统是否都写入数据 ProxySQL>select * from mysql_replication_hostgroups; +------------------+------------------+------------+---------+ | writer_hostgroup | reader_hostgroup | check_type | comment | +------------------+------------------+------------+---------+ | 10 | 20 | read_only | proxy | +------------------+------------------+------------+---------+ 1 row in set (0.00 sec) ProxySQL>select * from runtime_mysql_replication_hostgroups; +------------------+------------------+------------+---------+ | writer_hostgroup | reader_hostgroup | check_type | comment | +------------------+------------------+------------+---------+ | 10 | 20 | read_only | proxy | +------------------+------------------+------------+---------+ 1 row in set (0.00 sec) ProxySQL>select * from mysql_replication_hostgroups; +------------------+------------------+------------+---------+ | writer_hostgroup | reader_hostgroup | check_type | comment | +------------------+------------------+------------+---------+ | 10 | 20 | read_only | proxy | +------------------+------------------+------------+---------+ 1 row in set (0.00 sec) 2)添加主从节点 insert into mysql_servers(hostgroup_id,hostname,port) values(10,'192.168.30.40',3306); insert into mysql_servers(hostgroup_id,hostname,port) values(20,'192.168.30.46',3306); load mysql servers to runtime; save mysql servers to disk; ###查看状态 ProxySQL>select * from mysql_servers; +--------------+---------------+------+-----------+--------+--------+-------------+-----------------+---------------------+---------+----------------+---------+ | hostgroup_id | hostname | port | gtid_port | status | weight | compression | max_connections | max_replication_lag | use_ssl | max_latency_ms | comment | +--------------+---------------+------+-----------+--------+--------+-------------+-----------------+---------------------+---------+----------------+---------+ | 20 | 192.168.30.40 | 3306 | 0 | ONLINE | 1 | 0 | 1000 | 0 | 0 | 0 | | | 20 | 192.168.30.46 | 3306 | 0 | ONLINE | 1 | 0 | 1000 | 0 | 0 | 0 | | +--------------+---------------+------+-----------+--------+--------+-------------+-----------------+---------------------+---------+----------------+---------+ 2 rows in set (0.00 sec) 3)为ProxySQL创建监控用户 ###创建监控用户(在ProxySQL创建) set mysql-monitor_username='monitor'; set mysql-monitor_password='MySQL_123'; load mysql variables to runtime; save mysql variables to disk; ###对连接用户监控 ProxySQL>select * from monitor.mysql_server_connect_log; +---------------+------+------------------+-------------------------+------------------------------------------------------------------------+ | hostname | port | time_start_us | connect_success_time_us | connect_error | +---------------+------+------------------+-------------------------+------------------------------------------------------------------------+ | 192.168.30.40 | 3306 | 1664183920198998 | 0 | Access denied for user 'monitor'@'192.168.30.41' (using password: YES) | | 192.168.30.46 | 3306 | 1664183920926981 | 0 | Access denied for user 'monitor'@'HAMI02' (using password: YES) | | 192.168.30.46 | 3306 | 1664183970671663 | 1616 | NULL | | 192.168.30.40 | 3306 | 1664183971672625 | 2089 | NULL | +---------------+------+------------------+-------------------------+------------------------------------------------------------------------+ 4 rows in set (0.00 sec) 注:在没有创建监控用户前,会有很多的connect_error,这是因为没有配置监控信息时的错误,配置后如果connect_error的结果为NULL,则表示正常。 ###对心跳信息的监控 ProxySQL>select * from mysql_server_ping_log limit 10; +---------------+------+------------------+----------------------+------------------------------------------------------------------------+ | hostname | port | time_start_us | ping_success_time_us | ping_error | +---------------+------+------------------+----------------------+------------------------------------------------------------------------+ | 192.168.30.40 | 3306 | 1664183880229349 | 0 | Access denied for user 'monitor'@'192.168.30.41' (using password: YES) | | 192.168.30.46 | 3306 | 1664183880427787 | 0 | Access denied for user 'monitor'@'HAMI02' (using password: YES) | | 192.168.30.40 | 3306 | 1664183890229405 | 0 | Access denied for user 'monitor'@'192.168.30.41' (using password: YES) | | 192.168.30.46 | 3306 | 1664183890336793 | 0 | Access denied for user 'monitor'@'HAMI02' (using password: YES) | | 192.168.30.40 | 3306 | 1664183900229529 | 0 | Access denied for user 'monitor'@'192.168.30.41' (using password: YES) | | 192.168.30.46 | 3306 | 1664183900357491 | 0 | Access denied for user 'monitor'@'HAMI02' (using password: YES) | | 192.168.30.46 | 3306 | 1664183910229710 | 0 | Access denied for user 'monitor'@'HAMI02' (using password: YES) | | 192.168.30.40 | 3306 | 1664183910406115 | 0 | Access denied for user 'monitor'@'192.168.30.41' (using password: YES) | | 192.168.30.46 | 3306 | 1664183920229740 | 0 | Access denied for user 'monitor'@'HAMI02' (using password: YES) | | 192.168.30.40 | 3306 | 1664183920346638 | 0 | Access denied for user 'monitor'@'192.168.30.41' (using password: YES) | +---------------+------+------------------+----------------------+------------------------------------------------------------------------+ 10 rows in set (0.00 sec) ###对read_only值监控 ProxySQL>select * from mysql_server_read_only_log limit 10; +---------------+------+------------------+-----------------+-----------+------------------------------------------------------------------------------------------------------------+ | hostname | port | time_start_us | success_time_us | read_only | error | +---------------+------+------------------+-----------------+-----------+------------------------------------------------------------------------------------------------------------+ | 192.168.30.46 | 3306 | 1664183876942878 | 0 | NULL | timeout on creating new connection: Access denied for user 'monitor'@'HAMI02' (using password: YES) | | 192.168.30.40 | 3306 | 1664183876961694 | 0 | NULL | timeout on creating new connection: Access denied for user 'monitor'@'192.168.30.41' (using password: YES) | | 192.168.30.46 | 3306 | 1664183878441697 | 0 | NULL | timeout on creating new connection: Access denied for user 'monitor'@'HAMI02' (using password: YES) | | 192.168.30.40 | 3306 | 1664183878461063 | 0 | NULL | timeout on creating new connection: Access denied for user 'monitor'@'192.168.30.41' (using password: YES) | | 192.168.30.46 | 3306 | 1664183879941587 | 0 | NULL | timeout on creating new connection: Access denied for user 'monitor'@'HAMI02' (using password: YES) | | 192.168.30.40 | 3306 | 1664183879961993 | 0 | NULL | timeout on creating new connection: Access denied for user 'monitor'@'192.168.30.41' (using password: YES) | | 192.168.30.46 | 3306 | 1664183881441750 | 0 | NULL | timeout on creating new connection: Access denied for user 'monitor'@'HAMI02' (using password: YES) | | 192.168.30.40 | 3306 | 1664183881461890 | 0 | NULL | timeout on creating new connection: Access denied for user 'monitor'@'192.168.30.41' (using password: YES) | | 192.168.30.40 | 3306 | 1664183882942044 | 0 | NULL | timeout on creating new connection: Access denied for user 'monitor'@'192.168.30.41' (using password: YES) | | 192.168.30.46 | 3306 | 1664183882958866 | 0 | NULL | timeout on creating new connection: Access denied for user 'monitor'@'HAMI02' (using password: YES) | +---------------+------+------------------+-----------------+-----------+------------------------------------------------------------------------------------------------------------+ 10 rows in set (0.00 sec) 注:monitor就会开始监控后端的read_only值,然后按照read_only的值将某些节点自动移到读写组。 4)为ProxySQL配置对外访问用户 insert into mysql_users(username,password,default_hostgroup) values('proxysql','MySQL_123',10); insert into mysql_users(username,password,default_hostgroup) values('proxysql2','*0815E74A768849A6CCF0E9C1C5B940FB4D9F839E',20); load mysql users to runtime; save mysql users to disk; ProxySQL>select * from mysql_users\G *************************** 1. row *************************** username: proxysql password: MySQL_123 active: 1 use_ssl: 0 default_hostgroup: 10 default_schema: NULL schema_locked: 0 transaction_persistent: 1 fast_forward: 0 backend: 1 frontend: 1 max_connections: 10000 attributes: comment: *************************** 2. row *************************** username: proxysql2 password: *0815E74A768849A6CCF0E9C1C5B940FB4D9F839E active: 1 use_ssl: 0 default_hostgroup: 20 default_schema: NULL schema_locked: 0 transaction_persistent: 1 fast_forward: 0 backend: 1 frontend: 1 max_connections: 10000 attributes: comment: 2 rows in set (0.00 sec) mysql_users 表最主要的三个字段 username、password、default_hostgroup 解释。 username:前端连接 ProxySQL 的数据库用户 password:用户对应的密码,即可以是明文密码,也可以是 hash 密码,如果想使用 hash 密码,可以先在某个节点上执行 select password('password'),然后将加密结果复制到该字段。 default_hostgroup:用户默认的路由目标,例如:若用户 proxysql2 是个只读用户,则该字段值可以设置为20,表示所有的SQL语句默认情况下将路由到 hostgroup_id=20 的组;若用户不是只读用户,则该字段必须设置为10,表示 DML 语句会路由到 hostgroup_id=10 的组,查询语句即会路由到 hostgroup_id=10 的组,也会路由到 hostgroup_id=20 的组。 5.6 配置读写分离策略 ProxySQL 的路由规则配置比较灵活,可以基于用户级别,数据库级别等。由于 StoneDB 提供的是 OLAP 分析型查询业务场景,建议将聚合类查询、即席查询、复杂查询等分发到 StoneDB。由于只是测试,因此只配置了几个简单的路由规则。 与查询规则有关的表有两个:mysql_query_rules 和 mysql_query_rules_fast_routing,表mysql_query_rules_fast_routing 是 mysql_query_rules 的扩展,并在以后评估快速路由策略和属性(仅在ProxySQL 1.4.7+中可用)。 mysql_query_rules 表的几个字段解释。 active:是否启用这个规则,1表示启用,0表示禁用 match_pattern:设置规则 destination_hostgroup:默认指定的分组 apply:真正执行应用规则 ###创建规则 这里我创建两个规则: 1)把所有以select开头的语句分配到编号为20的读组中; 2)把select...for update语句分配到编号为10的写组中,其他所有操作都会默认路由到写组。 insert into mysql_query_rules(rule_id,active,match_pattern,destination_hostgroup,apply) values(1,1,'^select.*for update$',10,1); insert into mysql_query_rules(rule_id,active,match_pattern,destination_hostgroup,apply) values(2,1,'^select',20,1); insert into mysql_query_rules(rule_id,active,match_pattern,destination_hostgroup,apply) values(3,1,'^select*sum',20,1); load mysql query rules to runtime; save mysql query rules to disk; ###查看规则 ProxySQL>select * from mysql_query_rules\G *************************** 1. row *************************** rule_id: 1 active: 1 username: NULL schemaname: NULL flagIN: 0 client_addr: NULL proxy_addr: NULL proxy_port: NULL digest: NULL match_digest: NULL match_pattern: ^select.*for update$ negate_match_pattern: 0 re_modifiers: CASELESS flagOUT: NULL replace_pattern: NULL destination_hostgroup: 10 cache_ttl: NULL cache_empty_result: NULL cache_timeout: NULL reconnect: NULL timeout: NULL retries: NULL delay: NULL next_query_flagIN: NULL mirror_flagOUT: NULL mirror_hostgroup: NULL error_msg: NULL OK_msg: NULL sticky_conn: NULL multiplex: NULL gtid_from_hostgroup: NULL log: NULL apply: 1 attributes: comment: NULL *************************** 2. row *************************** rule_id: 2 active: 1 username: NULL schemaname: NULL flagIN: 0 client_addr: NULL proxy_addr: NULL proxy_port: NULL digest: NULL match_digest: NULL match_pattern: ^select negate_match_pattern: 0 re_modifiers: CASELESS flagOUT: NULL replace_pattern: NULL destination_hostgroup: 20 cache_ttl: NULL cache_empty_result: NULL cache_timeout: NULL reconnect: NULL timeout: NULL retries: NULL delay: NULL next_query_flagIN: NULL mirror_flagOUT: NULL mirror_hostgroup: NULL error_msg: NULL OK_msg: NULL sticky_conn: NULL multiplex: NULL gtid_from_hostgroup: NULL log: NULL apply: 1 attributes: comment: NULL *************************** 3. row *************************** rule_id: 3 active: 1 username: NULL schemaname: NULL flagIN: 0 client_addr: NULL proxy_addr: NULL proxy_port: NULL digest: NULL match_digest: NULL match_pattern: ^select*sum negate_match_pattern: 0 re_modifiers: CASELESS flagOUT: NULL replace_pattern: NULL destination_hostgroup: 20 cache_ttl: NULL cache_empty_result: NULL cache_timeout: NULL reconnect: NULL timeout: NULL retries: NULL delay: NULL next_query_flagIN: NULL mirror_flagOUT: NULL mirror_hostgroup: NULL error_msg: NULL OK_msg: NULL sticky_conn: NULL multiplex: NULL gtid_from_hostgroup: NULL log: NULL apply: 1 attributes: comment: NULL 3 rows in set (0.00 sec) 注:select...for update 规则的 rule_id 必须要小于普通的 select 规则的 rule_id,因为 ProxySQL 是根据 rule_id的顺序进行规则匹配的。 5.7 读写分离测试 1)读操作 # mysql -uproxysql -pMySQL_123 -h127.0.0.1 -P6033 mysql: [Warning] Using a password on the command line interface can be insecure. Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 6 Server version: 5.5.30 (ProxySQL) Copyright (c) 2000, 2021, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. proxysql@HAMI02 17:21: [(none)]> select sum(money) from aa.ttt; +------------+ | sum(money) | +------------+ | 88888.8 | +------------+ 1 row in set (0.01 sec) proxysql@HAMI02 17:21: [(none)]> select @@server_id; +-------------+ | @@server_id | +-------------+ | 46 | +-------------+ 1 row in set (0.00 sec) 2)写操作 # mysql -uproxysql -pMySQL_123 -h127.0.0.1 -P6033 mysql: [Warning] Using a password on the command line interface can be insecure. Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 19 Server version: 5.5.30 (ProxySQL) Copyright (c) 2000, 2021, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. proxysql@HAMI02 19:53: [(none)]> begin; Query OK, 0 rows affected (0.00 sec) proxysql@HAMI02 19:54: [(none)]> insert into aa.t1 values(7); Query OK, 1 row affected (0.00 sec) proxysql@HAMI02 19:54: [(none)]> select @@server_id; +-------------+ | @@server_id | +-------------+ | 40 | +-------------+ 1 row in set (0.00 sec) 6、建议项 1)建议设置为 GTID 模式,好处是从库发生 crash 后,不需要去找位点,系统会自动拉起复制线程; 2)在从库的并行 worker 相同的情况下,从库 StoneDB 相比从库 InnoDB 会消耗更多的CPU资源,如果未来上生产环境,建议 StoneDB 的配置略比 InnoDB 的配置高; 3)若从库 StoneDB 的延迟较高,可临时关闭参数 tianmu_enable_rowstore,待追上主库后再开启参数。关闭该参数带来的风险是如果在同步过程中 StoneDB 发生重启,重启后会出现丢数据的情况; 4)建议将聚合类查询、即席查询、复杂查询等分发到 StoneDB。

优秀的个人博客,低调大师

Optional源码解析与实践

1 导读 NullPointerException在开发过程中经常遇到,稍有不慎小BUG就出现了,如果避免这个问题呢,Optional就是专门解决这个问题的类,那么Optional如何使用呢?让我们一起探索一下吧! 2 源码解析 2.1 Optional定义 Optional类是Java8为了解决null值判断问题而创建的容器类,在java.util 下,使用Optional类可以避免显式的null值判断,避免null导致的NullPointerException。首先,Optional是一个容器,它可以保存类型T的值,也可以为null的容器对象。Optional容器只能存一个值。 2.2 Optional的属性 1)源码: /** * Common instance for {@code empty()}. */ private static final Optional<?> EMPTY = new Optional<>(); /** * If non-null, the value; if null, indicates no value is present */ private final T value; 根据源码可以看到Optional有两个属性,一个是为空值准备的EMPTY和泛型值value; 2.3 Optional的方法 Optional除toString()、hashCode() 、equals()等Object的方法外,还包含以下方法。 2.3.1 私有构造方法 /** * Constructs an empty instance. * * @implNote Generally only one empty instance, {@link Optional#EMPTY}, * should exist per VM. */ private Optional() { this.value = null; } /** * Constructs an instance with the value present. * * @param value the non-null value to be present * @throws NullPointerException if value is null */ private Optional(T value) { this.value = Objects.requireNonNull(value); } 分别是创建一个空实例和构造一个具有当前值的实例。 2.3.2 创建方法 1)源码 public static<T> Optional<T> empty() { @SuppressWarnings("unchecked") Optional<T> t = (Optional<T>) EMPTY; return t; } public static <T> Optional<T> of(T value) { return new Optional<>(value); } public static <T> Optional<T> ofNullable(T value) { return value == null ? empty() : of(value); } 2)方法说明 empty(): 创建一个空的 Optional 实例 of(T t) : 创建一个 Optional 实例,当 t为null时抛出异常 ofNullable(T t): 创建一个 Optional 实例,但当 t为null时不会抛出异常,而是返回一个空的实例 3)测试代码 public static void main(String[] args) { Integer value1 = null; Integer value2 = 1; try { Optional<Integer> optional1 = Optional.empty(); System.out.println("optional1创建了"); }catch (Exception e){ System.out.println("optional1失败了"); } try { Optional<Integer> optional2 = Optional.of(value1); System.out.println("optional2创建了"); }catch (Exception e){ System.out.println("optional2失败了"); } try { Optional<Integer> optional3 = Optional.ofNullable(value1); System.out.println("optional3创建了"); }catch (Exception e){ System.out.println("optional3失败了"); } try { Optional<Integer> optional4 = Optional.of(value2); System.out.println("optional4创建了"); }catch (Exception e){ System.out.println("optional4失败了"); } try { Optional<Integer> optional5 = Optional.ofNullable(value2); System.out.println("optional5创建了"); }catch (Exception e){ System.out.println("optional5失败了"); } } 4)运行结果 2.3.3 值获取方法 1)源码 public T get() { if (value == null) { throw new NoSuchElementException("No value present"); } return value; } 2)方法说明 get(): 如果Optional不为空,则返回该Optional容器中的值,否则抛出NoSuchElementExceptio 。 3)测试代码 public static void main(String[] args) { Integer value1 = null; Integer value2 = 1; Optional<Integer> optional1 = Optional.ofNullable(value1); Optional<Integer> optional2 = Optional.of(value2); try { Integer result=optional1.get(); System.out.println("optional1的值是:"+result); }catch (Exception e){ System.out.println("optional1的值获取失败,原因:"+e.getMessage()); } try { Integer result=optional2.get(); System.out.println("optional2的值是:"+result); }catch (Exception e){ System.out.println("optional2的值获取失败,原因:"+e.getMessage()); } } 4)运行结果 2.3.4 判断方法 1)源码 public boolean isPresent() { return value != null; } public void ifPresent(Consumer<? super T> consumer) { if (value != null) consumer.accept(value); } public T orElse(T other) { return value != null ? value : other; } public T orElseGet(Supplier<? extends T> other) { return value != null ? value : other.get(); } public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X { if (value != null) { return value; } else { throw exceptionSupplier.get(); } } 2)方法说明 isPresent(): 判断optional是否为空,如果空则返回false,否则返回true ifPresent(Consumer c): 如果optional不为空,则将optional中的对象传给Comsumer函数 orElse(T other): 如果optional不为空,则返回optional中的对象;如果为null,则返回 other 这个对象。 orElseGet(Supplier other): 如果optional不为空,则返回optional中的对象;如果为null,否则调用其他函数并返回调用的结果 orElseThrow(Supplier exception): 如果optional不为空,则返回optional中的对象;如果为null,则抛出Supplier函数生成的异常 3)测试代码 public static void main(String[] args) { Integer value1 = null; Integer value2 = 1; Optional<Integer> optional1 = Optional.ofNullable(value1); Optional<Integer> optional2 = Optional.of(value2); try { if(optional1.isPresent()){ System.out.println("optional1的isPresent结果不为空"); }else{ System.out.println("optional1的isPresent结果为空"); } }catch (Exception e){ System.out.println("optional1的isPresent判空失败,原因:"+e.getMessage()); } try { if(optional2.isPresent()){ System.out.println("optional2的isPresent结果不为空"); }else{ System.out.println("optional2的isPresent结果为空"); } }catch (Exception e){ System.out.println("optional2的isPresent判空失败,原因:"+e.getMessage()); } optional1.ifPresent(t->{ int i =t+1; System.out.println("optional1处理后的值是"+i); }); optional2.ifPresent(t->{ int i =t+1; System.out.println("optional2处理后的值是"+i);}); Integer value3 = 2; Integer result = optional1.orElse(value3); System.out.println("optional1执行orElse处理后的值是"+result); result = optional2.orElse(value3); System.out.println("optional2执行orElse处理后的值是"+result); result = optional1.orElseGet(()-> new Integer(-1)); System.out.println("optional1执行orElseGet处理后的值是"+result); result = optional2.orElseGet(()-> new Integer(-1)); System.out.println("optional2执行orElseGet处理后的值是"+result); try { result = optional1.orElseThrow (()-> new RuntimeException("值是空的")); System.out.println("optional1执行orElseThrow处理后的值是"+result); }catch (Exception e){ System.out.println("optional1的orElseThrow抛出异常:"+e.getMessage()); } try { result = optional2.orElseThrow (()-> new RuntimeException("值是空的")); System.out.println("optional2执行orElseThrow处理后的值是"+result); }catch (Exception e){ System.out.println("optional2的orElseThrow抛出异常:"+e.getMessage()); 4)运行结果 2.3.5 过滤方法 1)源码 public Optional<T> filter(Predicate<? super T> predicate) { Objects.requireNonNull(predicate); if (!isPresent()) return this; else return predicate.test(value) ? this : empty(); } 2)方法说明 filter(Predicate p): 如果optional不为空,则执行Predicate p,如果p的结果为true,则返回原本的optional,否则返回空的optional 3)测试代码 public static void main(String[] args) { Integer value1 = 5; Integer value2 = 6; Optional<Integer> optional1 = Optional.ofNullable(value1); Optional<Integer> optional2 = Optional.of(value2); Optional<Integer> result =optional1.filter(t->t > 5); System.out.println("optional1的filter后的值:"+result); result =optional2.filter(t->t > 5); System.out.println("optional2的filter后的值:"+result); 4)运行结果 2.3.6 映射方法 1)源码 public<U> Optional<U> map(Function<? super T, ? extends U> mapper) { Objects.requireNonNull(mapper); if (!isPresent()) return empty(); else { return Optional.ofNullable(mapper.apply(value)); } } public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) { Objects.requireNonNull(mapper); if (!isPresent()) return empty(); else { return Objects.requireNonNull(mapper.apply(value)); } } 2)方法说明 map(Function mapper): 如果存在一个值,则对其应用提供的映射函数,如果结果非空,则返回描述结果的Optional。 否则返回一个空的Optional。 flatMap(Function< T,Optional> mapper): 如果有值,则对其应用提供的可选映射函数,返回结果,否则返回空的可选函数。 这个方法类似于map(Function),但是提供的映射器的结果已经是一个可选的,如果调用,flatMap不会用额外的可选的包装它。 区别:map会自动将u放到optional中,而flatMap则需要手动给u创建一个optional 3)测试代码 public static void main(String[] args) { User user1 = null; User user2 = new User("user2名字",19); Optional<User> optional1 = Optional.ofNullable(user1); Optional<User> optional2 = Optional.of(user2); System.out.println("=========map=========="); System.out.println("optional1的map前的值:"+optional1); Optional<String> result =optional1.map(t->t.getName()); System.out.println("optional1的map后的值:"+result); System.out.println("optional2的map前的值:"+optional2); result =optional2.map(t->t.getName()); System.out.println("optional2的map后的值:"+result); System.out.println("===========flatMap========"); System.out.println("optional1的flatMap前的值:"+optional1); Optional<Integer> result2 =optional1.flatMap(t->Optional.ofNullable(t.getAge())); System.out.println("optional1的flatMap后的值:"+result2); System.out.println("optional2的flatMap前的值:"+optional2); result2 =optional2.flatMap(t->Optional.ofNullable(t.getAge())); System.out.println("optional2的flatMap后的值:"+result2); } public class User { String name; Integer age; public User(String name,Integer age){ this.name = name; this.age=age; } public String getName() { return name; } public Integer getAge() { return age; 4)运行结果 3 应用实例 3.1 错误用法 由于Optional并没有实现Serializable接口,所以不能作为类的属性。 不要把Optional作为方法的参数。 把if(x!=null)直接换成Optional.ofNullable(x).isPresent(),这样有过度编码的嫌疑。 直接使用Optional.get()的返回值进行操作,String result =Optional.ofNullable(null).get().toString();这样还是会抛出异常的。 3.2 建议用法 A类有属性B类,B类有属性C类,C类有name这个字段。 使用Optional之前: if(atest!=null){ Btest btest =atest.getBtest(); if(btest!=null){ Ctest ctest = btest.getCtest(); if (ctest != null) { name =ctest.getName(); } } } 使用Optional之后: name = Optional.ofNullable(atest).map(t->t.getBtest()).map(t->t.getCtest()).map(t->t.getName()).orElse("默认值"); 代码是不是看上去更整洁了呢? 4 总结 通过对Optional源码解析和用例测试代码的运行结果,可以看出使用Optional可以优化null值判断代码,让代码变得更加优雅和整洁。 作者:陈昌浩

优秀的个人博客,低调大师

Kubernetes 集群无损升级实践

一、背景 活跃的社区和广大的用户群,使 Kubernetes 仍然保持3个月一个版本的高频发布节奏。高频的版本发布带来了更多的新功能落地和 bug 及时修复,但是线上环境业务长期运行,任何变更出错都可能带来巨大的经济损失,升级对企业来说相对吃力,紧跟社区更是几乎不可能,因此高频发布和稳定生产之间的矛盾需要容器团队去衡量和取舍。 vivo 互联网团队建设大规模 Kubernetes 集群以来,部分集群较长时间一直使用 v1.10 版本,但是由于业务容器化比例越来越高,对大规模集群稳定性、应用发布的多样性等诉求日益攀升,集群升级迫在眉睫。集群升级后将解决如下问题: 高版本集群在大规模场景做了优化,升级可以解决一系列性能瓶颈问题。 高版本集群才能支持 OpenKruise 等 CNCF 项目,升级可以解决版本依赖问题。 高版本集群增加的新特性能够提高集群资源利用率,降低服务器成本同时提高集群效率。 公司内部维护多个不同版本集群,升级后减少集群版本碎片化,进一步降低运维成本。 这篇文章将会从0到1的介绍 vivo 互联网团队支撑在线业务的集群如何在不影响原有业务正常运行的情况下从 v1.10 版本升级到 v1.17 版本。之所以升级到 v1.17 而不是更高的 v1.18 以上版本, 是因为在 v1.18 版本引入的代码变动 [1] 会导致 extensions/v1beta1 等高级资源类型无法继续运行(这部分代码在 v1.18 版本删除)。 二、无损升级难点 容器集群搭建通常有二进制 systemd 部署和核心组件静态 Pod 容器化部署两种方式,集群 API 服务多副本对外负载均衡。两种部署方式在升级时没有太大区别,二进制部署更贴合早期集群,因此本文将对二进制方式部署的集群升级做分享。 对二进制方式部署的集群,集群组件升级主要是二进制的替换、配置文件的更新和服务的重启;从生产环境 SLO 要求来看,升级过程务必不能因为集群组件自身逻辑变化导致业务重启。因此升级的难点集中在下面几点: 首先,当前内部集群运行版本较低,但是运行容器数量却很多,其中部分仍然是单副本运行,为了不影响业务运行,需要尽可能避免容器重启,这无疑是升级中最大的难点,而在 v1.10 版本和 v1.17 版本之间,kubelet 关于容器 Hash 值计算方式发生了变化,也就是说一旦升级必然会触发 kubelet 重新启动容器。 其次,社区推荐的方式是基于偏差策略 [2] 的升级以保证高可用集群升级同时不会因为 API resources 版本差异导致 kube-apiserve 和 kubelet 等组件出现兼容性错误,这就要求每次升级组件版本不能有2个 Final Release 以上的偏差,比如直接从 v1.11 升级至 v1.13是不推荐的。 再次,升级过程中由于新特性的引入,API 兼容性可能引发旧版本集群的配置不生效,为整个集群埋下稳定性隐患。这便要求在升级前尽可能的熟悉升级版本间的 ChangeLog,排查出可能带来潜在隐患的新特性。 三、无损升级方案 针对前述的难点,本节将逐个提出针对性解决方案,同时也会介绍升级后遇到的高版本 bug 和解决方法。希望关于升级前期兼容性筛查和升级过程中排查的问题能够给读者带来启发。 3.1 升级方式 在软件领域,主流的应用升级方式有两种,分别是原地升级和替换升级。目前这两种升级方式在业内互联网大厂均有采用,具体方案选择与集群上业务有很大关系。 替换升级 1)Kubernetes 替换升级是先准备一个高版本集群,对低版本集群通过逐个节点排干、删除最后加入新集群的方式将低版本集群内节点逐步轮换升级到新版本。 2)替换升级的优点是原子性更强,逐步升级各个节点,升级过程不存在中间态,对业务安全更有保障;缺点是集群升级工作量较大,排干操作对pod重启敏感度高的应用、有状态应用、单副本应用等都不友好。 原地升级 1)Kubernetes 原地升级是对节点上服务如 kube-controller-manager、 kubelet 等组件按照一定顺序批量更新,从节点角色维度批量管理组件版本。 2)原地升级的优点是自动化操作便捷,并且通过适当的修改能够很好的保证容器的生命周期连续性;缺点是集群升级中组件升级顺序很重要,升级中存在中间态,并且一个组件重启失败可能影响后续其他组件升级,原子性差。 vivo 容器集群上运行的部分业务对重启容忍度较低,尽可能避免容器重启是升级工作的第一要务。当解决好升级版本带来的容器重启后,结合业务容器化程度和业务类型不同,因地制宜的选择升级方式即可。二进制部署集群建议选择原地升级的方式,具有时间短,操作简捷,单副本业务不会被升级影响的好处。 3.2 跨版本升级 由于Kubernetes 本身是基于 API 的微服务架构,Kuberntes 内部架构也是通过 API 的调用和对资源对象的 List-Watch 来协同资源状态,因此社区开发者在设计 API 时遵循向上或向下兼容的原则。这个兼容性规则也是遵循社区的偏差策略 [2],即 API groups 弃用、启用时,对于 Alpha 版本会立即生效,对于 Beta 版本将会继续支持3个版本,超过对应版本将导致 API resource version 不兼容。例如 kubernetes 在 v1.16 对 Deployment 等资源的 extensions/v1beta1 版本执行了弃用,在v1.18 版本从代码级别执行了删除,当跨3个版本以上升级时会导致相关资源无法被识别,相应的增删改查操作都无法执行。 如果按照官方建议的升级策略,从 v1.10 升级到 v1.17 需要经过至少 7 次升级,这对于业务场景复杂的生产环境来说运维复杂度高,业务风险大。 对于类似的 API breaking change 并不是每个版本都会存在,社区建议的偏差策略是最安全的升级策略,经过细致的 Change Log 梳理和充分的跨版本测试,我们确认这几个版本之间不能存在影响业务运行和集群管理操作的 API 兼容性问题,对于 API 类型的废弃,可以通过配置 apiserver 中相应参数来启动继续使用,保证环境业务继续正常运行。 3.3 避免容器重启 在初步验证升级方案时发现大量容器都被重建,重启原因从升级后 kubelet 组件日志看到是 "Container definition changed"。结合源码报错位于 pkg/kubelet/kuberuntime_manager.go 文件 computePodActions 方法,该方法用来计算 pod 的 spec 哈希值是否发生变化,如果变化则返回 true,告知 kubelet syncPod 方法触发 pod 内容器重建或者 pod 重建。 kubelet 容器 Hash 计算; func (m *kubeGenericRuntimeManager) computePodActions(pod *v1.Pod, podStatus *kubecontainer.PodStatus) podActions { restart := shouldRestartOnFailure(pod) if _, _, changed := containerChanged(&container, containerStatus); changed { message = fmt.Sprintf("Container %s definition changed", container.Name) // 如果 container spec 发生变化,将会强制重启 container(将 restart 标志位设置为 true) restart = true } ... if restart { message = fmt.Sprintf("%s, will be restarted", message) // 需要重启的 container 加入到重启列表 changes.ContainersToStart = append(changes.ContainersToStart, idx) } } func containerChanged(container *v1.Container, containerStatus *kubecontainer.ContainerStatus) (uint64, uint64, bool) { // 计算 container spec 的 Hash 值 expectedHash := kubecontainer.HashContainer(container) return expectedHash, containerStatus.Hash, containerStatus.Hash != expectedHash } 相对于 v1.10 版本,v1.17 版本在计算容器 Hash 时使用的是 container 结构 json 序列化后的数据,而不是 v1.10 版本使用 container struct 的结构数据。而且高版本 kubelet 中对容器的结构也增加了新的属性,通过 go-spew 库计算出结果自然不一致,进一步向上传递返回值使得 syncPod 方法触发容器重建。 那是否可以通过修改 go-spew 对 container struct 的数据结构剔除新增的字段呢? 答案是肯定的,但是却不是优雅的方式,因为这样对核心代码逻辑侵入较为严重,以后每个版本的升级都需要定制代码,并且新增的字段越来越多,维护复杂度也会越来越高。换个角度,如果在升级过渡期间将属于旧版本集群 kubelet 创建的 Pod 跳过该检查,则可以避免容器重启。 和圈内同事交流后发现类似思路在社区已有实现,本地创建一个记录旧集群版本信息和启动时间的配置文件,kubelet 代码中维护一个 cache 读取配置文件,在每个 syncPod 周期中,当 kubelet 发现自身 version 高于 cache 中记录的 oldVersion, 并且容器启动时间早于当前 kubelet 启动时间,则会跳过容器 Hash 值计算。升级后的集群内运行定时任务探测 Pod 的 containerSpec 是否与高版本计算方式计算得到 Hash 结果全部一致,如果是则可以删除掉本地配置文件,syncPod 逻辑恢复到与社区完全一致。 具体方案参考这种实现的好处是对原生 kubelet 代码侵入小,没有改变核心代码逻辑,而且未来如果还需要升级高版本也可以复用该代码。如果集群内所有 Pod 都是当前版本 kubelet 创建,则会恢复到社区自身的逻辑。 3.4 Pod 非预期驱逐问题 Kubernetes 虽然迭代了十几个版本,但是每个迭代社区活跃度仍然很高,保持着每个版本大约30个关于拓展性增强和稳定性提升的新特性。选择升级很大一方面原因是引入很多社区开发的新特性来丰富集群的功能与提升集群稳定性。新特性开发也是遵循偏差策略,跨大版本升级很可能导致在部分配置未加载的情况下启用新特性,这就给集群带来稳定性风险,因此需要梳理影响 Pod 生命周期的一些特性,尤其关注控制器相关的功能。 这里注意到在 v1.13 版本引入的 TaintBasedEvictions 特性用于更细粒度的管理 Pod 的驱逐条件。在 v1.13基于条件版本之前,驱逐是基于 NodeController 的统一时间驱逐,节点 NotReady 超过默认5分钟后,节点上的 Pod 才会被驱逐;在 v1.16 默认开启 TaintBasedEvictions 后,节点 NotReady 的驱逐将会根据每个 Pod 自身配置的 TolerationSeconds 来差异化的处理。 旧版本集群创建的 Pod 默认没有设置 TolerationSeconds,一旦升级完毕 TaintBasedEvictions 被开启,节点变成 NotReady 后 5 秒就会驱逐节点上的 Pod。对于短暂的网络波动、kubelet 重启等情况都会影响集群中业务的稳定性。 TaintBasedEvictions 对应的控制器是按照 pod 定义中的 tolerationSeconds 决定 Pod 的驱逐时间,也就是说只要正确设置 Pod 中的 tolerationSeconds 就可以避免出现 Pod 的非预期驱逐。 在v1.16 版本社区默认开启的 DefaultTolerationSeconds 准入控制器基于 k8s-apiserver 输入参数 default-not-ready-toleration-seconds 和 default-unreachable-toleration-seconds 为 Pod 设置默认的容忍度,以容忍 notready:NoExecute 和 unreachable:NoExecute 污点。 新建 Pod 在请求发送后会经过 DefaultTolerationSeconds 准入控制器给 pod 加上默认的 tolerations。但是这个逻辑如何对集群中已经创建的 Pod 生效呢?查看该准入控制器发现除了支持 create 操作,update 操作也会更新 pod 定义触发 DefaultTolerationSeconds 插件去设置 tolerations。因此我们通过给集群中已经运行的 Pod 打 label 就可以达成目的。 tolerations: - effect: NoExecute key: node.kubernetes.io/not-ready operator: Exists tolerationSeconds: 300 - effect: NoExecute key: node.kubernetes.io/unreachable operator: Exists tolerationSeconds: 300 3.5 Pod MatchNodeSelector 为了判断升级时 Pod 是否发生非预期的驱逐以及是否存在 Pod 内容器批量重启,有脚本去实时同步节点上非Running状态的Pod和发生重启的容器。 在升级过程中,突然多出来数十个 pod 被标记为 MatchNodeSelector 状态,查看该节点上业务容器确实停止。kubelet 日志中看到如下错误日志; predicate.go:132] Predicate failed on Pod: nginx-7dd9db975d-j578s_default(e3b79017-0b15-11ec-9cd4-000c29c4fa15), for reason: Predicate MatchNodeSelector failed kubelet_pods.go:1125] Killing unwanted pod "nginx-7dd9db975d-j578s" 经分析,Pod 变成 MatchNodeSelector 状态是因为 kubelet 重启时对节点上 Pod 做准入检查时无法找到节点满足要求的节点标签,pod 状态就会被设置为 Failed 状态,而 Reason 被设置为 MatchNodeSelector。在 kubectl 命令获取时,printer 做了相应转换直接显示了Reason,因此我们看到 Pod 状态是 MatchNodeSelector。通过给节点加上标签,可以让 Pod 重新调度回来,然后删除掉 MatchNodeSelector 状态的 Pod 即可。 建议在升级前写脚本检查节点上 pod 定义中使用的 NodeSelector 属性节点是否都有对应的 Label。 3.6 无法访问 kube-apiserver 预发环境升级后的集群运行在 v1.17 版本后,突然有节点变成 NotReady 状态告警,分析后通过重启 kubelet 节点恢复正常。继续分析出错原因发现 kubelet 日志中出现了大量 use of closed network connection 报错。在社区搜索相关 issue 发现有类似的问题,其中有开发者描述了问题的起因和解决办法,并且在 v1.18 已经合入了代码。 问题的起因是 kubelet 默认连接是 HTTP/2.0 长连接,在构建 client 到 server的连接时使用的 golang net/http2 包存在 bug,在 http 连接池中仍然能获取到 broken 的连接,也就导致 kubelet 无法正常与 kube-apiserver 通信。 golang社区通过增加 http2 连接健康检查规避这个问题,但是这个 fix 仍然存在 bug ,社区在 golang v1.15.11 版本彻底修复。我们内部通过 backport 到 v1.17 分支,并使用 golang 1.15.15 版本编译二进制解决了此问题。 3.7 TCP 连接数问题 在预发布环境测试运行期间,偶然发现集群每个节点 kubelet 都有近10个长连接与 kube-apiserver 通信,这与我们认知的 kubelet 会复用连接与 kube-apiserver 通信明显不符,查看 v1.10 版本环境也确实只有1个长连接。这种 TCP 连接数增加情况无疑会对 LB 造成了压力,随着节点增多,一旦 LB 被拖垮,kubelet 无法上报心跳,节点会变成 NotReady,紧接着将会有大量 Pod 被驱逐,后果是灾难性的。因此除去对 LB 本身参数调优外,还需要定位清楚kubelet 到 kube-apiserver 连接数增加的原因。 在本地搭建的 v1.17.1 版本 kubeadm 集群 kubelet 到 kube-apiserver 也仅有1个长连接,说明这个问题是在 v1.17.1 到升级目标版本之间引入的,排查后(问题)发现增加了判断逻辑导致 kubelet 获取 client 时不再从 cache 中获取缓存的长连接。transport 的主要功能其实就是缓存了长连接,用于大量 http 请求场景下的连接复用,减少发送请求时 TCP(TLS) 连接建立的时间损耗。在该 PR 中对 transport 自定义 RoundTripper 的接口,一旦 tlsConfig 对象中有 Dial 或者 Proxy 属性,则不使用 cache 中的连接而新建连接。 // client-go 从 cache 获取复用连接逻辑 func tlsConfigKey(c *Config) (tlsCacheKey, bool, error) { ... if c.TLS.GetCert != nil || c.Dial != nil || c.Proxy != nil { // cannot determine equality for functions return tlsCacheKey{}, false, nil } ... } func (c *tlsTransportCache) get(config *Config) (http.RoundTripper, error) { key, canCache, err := tlsConfigKey(config) ... if canCache { // Ensure we only create a single transport for the given TLS options c.mu.Lock() defer c.mu.Unlock() // See if we already have a custom transport for this config if t, ok := c.transports[key]; ok { return t, nil } } ... } // kubelet 组件构建 client 逻辑 func buildKubeletClientConfig(ctx context.Context, s *options.KubeletServer, nodeName types.NodeName) (*restclient.Config, func(), error) { ... kubeClientConfigOverrides(s, clientConfig) closeAllConns, err := updateDialer(clientConfig) ... return clientConfig, closeAllConns, nil } // 为 clientConfig 设置 Dial属性,因此 kubelet 构建 clinet 时会新建 transport func updateDialer(clientConfig *restclient.Config) (func(), error) { if clientConfig.Transport != nil || clientConfig.Dial != nil { return nil, fmt.Errorf("there is already a transport or dialer configured") } d := connrotation.NewDialer((&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext) clientConfig.Dial = d.DialContext return d.CloseAll, nil 在这里构建 closeAllConns 对象来关闭已经处于 Dead 但是尚未 Close 的连接,但是上一个问题通过升级 golang 版本解决了这个问题,因此我们在本地代码分支回退了该修改中的部分代码解决了 TCP 连接数增加的问题。 最近追踪社区发现已经合并了解决方案 ,通过重构 client-go 的接口实现对自定义 RESTClient 的 TCP 连接复用。 四、无损升级操作 跨版本升级最大的风险是升级前后对象定义不一致,可能导致升级后的组件无法解析保存在 ETCD 数据库中的对象;也可能是升级存在中间态,kubelet 还未升级而控制平面组件升级,存在上报状态异常,最坏的情况是节点上 Pod 被驱逐。这些都是升级前需要考虑并通过测试验证的。 经过反复测试,上述问题在 v1.10 到 v1.17 之间除了部分废弃的 API Resources 通过增加 kube-apiserver 配置方式其他情况暂时不存在。为了保证升级时及时能处理未覆盖到的特殊情况,强烈建议升级前备份 ETCD 数据库,并在升级期间停止控制器和调度器,避免非预期的控制逻辑发生(实际上这里应该是停止 controller manager 中的部分控制器,不过需要修改代码编译临时 controller manager ,增加了升级流程步骤和管理复杂度,因此直接停掉了全局控制器)。 除却以上代码变动和升级流程注意事项,在替换二进制升级前,就剩下比对新老版本服务的配置项的区别以保证服务成功启动运行。对比后发现,kubelet 组件启动时不再支持 --allow-privileged 参数,需要删除。值得说明的是,删除不代表高版本不再支持节点上运行特权容器,在 v1.15 以后通过 Pod Security Policy 资源对象来定义一组 pod 访问的安全特征,更细粒度的做安全管控。 基于上面讨论的无损升级代码侧的修改编译二进制,再对集群组件配置文件中各个配置项修改后,就可以着手线上升级。整个升级步骤为: 备份集群(二进制,配置文件,ETCD数据库等); 灰度升级部分节点,验证二进制和配置文件正确性 提前分发升级的二进制文件; 停止控制器、调度器和告警; 更新控制平面服务配置文件,升级组件; 更新计算节点服务配置文件,升级组件; 为节点打 Label 触发 pod 增加 tolerations 属性; 打开控制器和调度器,启用告警; 集群业务点检,确认集群正常。 升级过程中建议节点并发数不要太高,因为大量节点 kubelet 同时重启上报信息,对 kube-apiserver 前面使用的 LB 带来冲击,特别情况下可能节点心跳上报失败,节点状态会在 NotReady 与 Ready 状态间跳动。 五、总结 集群升级是困扰容器团队比较长时间的事,在经过一系列调研和反复测试,解决了上面提到的数个关键问题后,成功将集群从 v1.10 升级到 v1.17 版本,1000 个节点的集群分批执行升级操作,大概花费 10 分钟,后续在完成平台接口改造后将会再次升级到更高版本。 集群版本升级提高了集群的稳定性、增加了集群的扩展性,同时还丰富了集群的能力,升级后的集群也能够更好的兼容 CNCF 项目。 如开篇所述,按照偏差策略频繁对大规模集群升级可能不太现实,因此跨版本升级虽然风险较大,但是也是业界广泛采用的方式。在 2021 年中国 KubeCon 大会上,阿里巴巴也有关于零停机跨版本升级 Kubernetes 集群的分享,主要是关于应用迁移、流量切换等升级关键点的介绍,升级的准备工作和升级过程相对复杂。相对于阿里巴巴的集群跨版本替换升级方案,原地升级的方式需要在源码上做少量修改,但是升级过程会更简单,运维自动化程度更高。 由于集群版本具有很大的可选择性,本文所述的升级并不一定广泛适用,笔者更希望给读者提供生产集群在跨版本升级时的思路和风险点。升级过程短暂,但是升级前的准备和调研工作是费时费力的,需要对不同版本 Kubernetes 特性和源码深入探索,同时对 Kubernetes 的 API 兼容性策略和发布策略拥有完整认知,这样便能在升级前做出充分的测试,也能更从容面对升级过程中突发情况。 六、参考链接 [1]https://github.com [2] https://kubernetes.io/version-skew-policy [3] 具体方案参考:https://github.comstart [4] 类似的问题: https://github.com/kubernetes [5] https://github.com/golang/34978 [6] https://github.com/kubernetes/100376 [7] https://github.com/kubernetes/95427 [8] https://github.com/kubernetes/105490 作者:vivo互联网服务器团队-Shu Yingya

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册