大数据 ETL 处理工具 Kettle 入门实践
Kettle 是什么Kettle 是一款国外开源的 ETL 工具,对商业用户也没有限制,纯 Java 编写,可以在 Window、Linux、Unix 上运行,绿色无需安装,数据抽取高效稳定。Kettle 中文名称叫水壶,它允许管理来自不同数据库的数据,把各种数据放到一个壶里,然后以一种指定的格式流出。Kettle 中有两种脚本文件,Transformation 和 Job, Transformation 完成针对数据的基础转换,Job 则完成整个工作流的控制。通过图形界面设计实现做什么业务,并在 Job 下的 start 模块,有一个定时功能,可以每日,每周等方式进行定时。Kettle 的核心组件名称功能Spoon通过图形接口,允许你通过图形界面来设计 ETL 转换过程(Transformation)Pan运行转换的命令行工具Kitchen运行作业的命令行工具CarteCarte 是一个轻量级别的 Web 容器,用于建立专用、远程的 ETL Server作业和转换可以在图形界面里执行,但这只适合在开发、测试和调试阶段。在开发完成后,需要部署到生产环境中  Spoon 就很少用到了,Kitchen 和 Pan 命令行工具用于实际的生产环境。部署生产阶段一般需要通过命令行执行,需要把命令行放到 Shell 脚本中,并定时调度这个脚本。Kitchen 和 Pan 工具是 Kettle 的命令行执行程序,只是在 Kettle 执行引擎上的封装,它们只是解释命令行参数,调用并把这些参数传递给 Kettle 引擎。Kitchen 和 Pan 在概念和用法上都非常相近,这两个命令的参数也基本是一样的。唯一不同的是 Kitchen 用于执行作业,Pan 用于执行转换。Kettle 概念模型Kettle 的执行分为两个层次:Job(作业,.kjb 后缀)和 Transformation(转换,.ktr 后缀)img简单地说,一个转换就是一个 ETL 的过程,而作业则是多个转换、作业的集合,在作业中可以对转换或作业进行调度、定时任务等。在实际过程中,写的流程不能很复杂,当数据抽取需要多步骤时,需要分成多个转换,在集成到一个作业里顺序摆放,然后执行即可。目录文件功能说明来源网络来源网络来源网络下载及安装官网各个版本下载地址:https://sourceforge.net/projects/pentaho/files/Data%20Integration/ 国内 Kettle 论坛网:https://www.kettle.net.cn/Kettle 是纯 Java 编程的开源软件,需要安装 JDK,并配置环境变量,解压后直接使用无需安装。需准备的其他东西:数据库驱动,如将驱动放在 Kettle 根目录的 bin 文件夹下面即可。打开 Kettle 只需要运行 Spoon.bat (win)/ spoon.sh (Linux / macOS),即可打开 Spoon 图形工具。启动 Kettle如下图,执行  ./spoon.sh 命令image-20210705214148018欢迎页面首页HelloWorld需求:把数据从 CSV 文件复制到 Excel 文件CSV 文件到 Excel 文件CSV 文件输入CSV 输入控件将 「CSV 文件输入」拖拽到右侧的工作区,双击进行编辑,浏览选择准备好的测试文件,点击「获取字段」自动获取 CSV 文件中表头信息,输入配置完成,下一步进行输出配置。编辑 CSV 文件输入Excel 输出Excel 输出将 「Excel 输出」拖拽到右侧的工作区,双击进行编辑,这步比较简单,浏览选择输出目录和设置文件名,完成配置。输出配置转换文件按住 shift + 鼠标左键可以建立连接,保存转换配置连接运行转换运行结果查看结果运行结果总结初步了解 Kettle 核心组件及其使用作业和转换可以在图形界面里执行,但这只适合在开发、测试和调试阶段。在开发完成后,需要部署到生产环境中  Spoon 就很少用到了,Kitchen 和 Pan 命令行工具用于实际的生产环境。部署生产阶段一般需要通过命令行执行,需要把命令行放到 Shell 脚本中,并定时调度这个脚本。Kitchen 和 Pan 工具是 Kettle 的命令行执行程序,只是在 Kettle 执行引擎上的封装,它们只是解释命令行参数,调用并把这些参数传递给 Kettle 引擎。Kitchen 和 Pan 在概念和用法上都非常相近,这两个命令的参数也基本是一样的。唯一不同的是 Kitchen 用于执行作业,Pan 用于执行转换。分步操作一个 HelloWrold 过程
Android C++ 系列:Linux 常用函数和工具
1. 时间函数1.1 文件访问时间#include <sys/types.h> #include <utime.h> int utime (const char *name, const struct utimebuf *t); 返回:若成功则为 0,若出错则为- 1如果times是一个空指针,则存取时间和修改时间两者都设置为当前时间;如果times是非空指针,则存取时间和修改时间被设置为 times所指向的结构中的值。此 时,进程的有效用户ID必须等于该文件的所有者 ID,或者进程必须是一个超级用户进程。对 文件只具有写许可权是不够的此函数所使用的结构是:struct utimbuf { time_t actime; /*access time*/ time_t modtime; /*modification time*/ }1.2 cpu使用时间#include <sys/time.h> #include <sys/resource.h> int getrusage(int who, struct rusage *usage);RUSAGE_SELF:Return resource usage statistics for the calling process, which is the sum of resources used by all threads in the process.RUSAGE_CHILDREN:Return resource usage statistics for all children of the calling process that have terminated and been waited for. These statis‐ tics will include the resources used by grandchildren, and fur‐ ther removed descendants, if all of the intervening descendants waited on their terminated children.RUSAGE_THREAD (since Linux 2.6.26):Return resource usage statistics for the calling thread.2. 网络工具2.1 ifconfigsudo ifconfig eth0 down/up sudo ifconfig eth0 192.168.102.1232.2 netstata (all)显示所有选项,默认不显示LISTEN相关t (tcp)仅显示tcp相关选项u (udp)仅显示udp相关选项n 拒绝显示别名,能显示数字的全部转化成数字。 -l 仅列出有在 Listen (监听) 的服務状态p 显示建立相关链接的程序名 -r 显示路由信息,路由表e 显示扩展信息,例如uid等 -s 按各个协议进行统计c 每隔一个固定时间,执行该netstat命令。LISTEN和LISTENING的状态只有用-a或者-l才能看到:sudo netstat -anp | grep ftp2.3 设置IP以DHCP方式配置网卡:1.编辑文件/etc/network/interfaces:sudo vi /etc/network/interfaces2.并用下面的行来替换有关eth0的行:# The primary network interface - use DHCP to find our address auto eth0 iface eth0 inet dhcp3.用下面的命令使网络设置生效:sudo /etc/init.d/networking restart4.也可以在命令行下直接输入下面的命令来获取地址:sudo dhclient eth0为网卡配置静态IP地址:1.编辑文件/etc/network/interfaces:sudo vi /etc/network/interfaces2.用下面的行来替换有关eth0的行:# The primary network interface auto eth0 iface eth0 inet static address 192.168.2.1 gateway 192.168.2.254 netmask 255.255.255.0 #network 192.168.2.0 #broadcast 192.168.2.2553.将上面的ip地址等信息换成你自己就可以了.用下面的命令使网络设置生效:sudo /etc/init.d/networking restart4.设置DNS:要访问DNS 服务器来进行查询,需要设置/etc/resolv.conf文件, 假设DNS服务器的IP地址是192.168.2.2, 那么/etc/resolv.conf文件的内容应为:nameserver 192.168.2.25.手动重启网络服务:sudo /etc/init.d/networking restart3. 总结本文介绍了Linux常用命令工具及函数:文件访问时间函数、cpu使用时间函数、ifconfig、netstat、设置IP方式等。
kafka万亿级消息实战(3)
1.9 安全认证是不是我们的集群所有人都可以随意访问呢?当然不是,为了集群的安全,我们需要进行权限认证,屏蔽非法操作。主要包括以下几个方面需要做安全认证:(1)生产者权限认证;(2)消费者权限认证;(3)指定数据目录迁移安全认证;官网地址:http://kafka.apache.org1.10 集群容灾跨机架容灾:官网地址:http://kafka.apache.org跨集群/机房容灾:如果有异地双活等业务场景时,可以参考Kafka2.7版本的MirrorMaker 2.0。GitHub地址:https://github.com精确KIP地址 :https://cwiki.apache.orgZooKeeper集群上Kafka元数据恢复:我们会定期对ZooKeeper上的权限信息数据做备份处理,当集群元数据异常时用于恢复。1.11 参数/配置优化broker服务参数优化:这里我只列举部分影响性能的核心参数。num.network.threads #创建Processor处理网络请求线程个数,建议设置为broker当CPU核心数*2,这个值太低经常出现网络空闲太低而缺失副本。 num.io.threads #创建KafkaRequestHandler处理具体请求线程个数,建议设置为broker磁盘个数*2 num.replica.fetchers #建议设置为CPU核心数/4,适当提高可以提升CPU利用率及follower同步leader数据当并行度。 compression.type #建议采用lz4压缩类型,压缩可以提升CPU利用率同时可以减少网络传输数据量。 queued.max.requests #如果是生产环境,建议配置最少500以上,默认为500。 log.flush.scheduler.interval.ms log.flush.interval.ms log.flush.interval.messages #这几个参数表示日志数据刷新到磁盘的策略,应该保持默认配置,刷盘策略让操作系统去完成,由操作系统来决定什么时候把数据刷盘; #如果设置来这个参数,可能对吞吐量影响非常大; auto.leader.rebalance.enable #表示是否开启leader自动负载均衡,默认true;我们应该把这个参数设置为false,因为自动负载均衡不可控,可能影响集群性能和稳定;生产优化:这里我只列举部分影响性能的核心参数。linger.ms #客户端生产消息等待多久时间才发送到服务端,单位:毫秒。和batch.size参数配合使用;适当调大可以提升吞吐量,但是如果客户端如果down机有丢失数据风险; batch.size #客户端发送到服务端消息批次大小,和linger.ms参数配合使用;适当调大可以提升吞吐量,但是如果客户端如果down机有丢失数据风险; compression.type #建议采用lz4压缩类型,具备较高的压缩比及吞吐量;由于Kafka对CPU的要求并不高,所以,可以通过压缩,充分利用CPU资源以提升网络吞吐量; buffer.memory #客户端缓冲区大小,如果topic比较大,且内存比较充足,可以适当调高这个参数,默认只为33554432(32MB) retries #生产失败后的重试次数,默认0,可以适当增加。当重试超过一定次数后,如果业务要求数据准确性较高,建议做容错处理。 retry.backoff.ms #生产失败后,重试时间间隔,默认100ms,建议不要设置太大或者太小。除了一些核心参数优化外,我们还需要考虑比如topic的分区个数和topic保留时间;如果分区个数太少,保留时间太长,但是写入数据量非常大的话,可能造成以下问题:1)topic分区集中落在某几个broker节点上,导致流量副本失衡;2)导致broker节点内部某几块磁盘读写超负载,存储被写爆;1.11.1 消费优化消费最大的问题,并且经常出现的问题就是消费延时,拉历史数据。当大量拉取历史数据时将出现大量读盘操作,污染pagecache,这个将加重磁盘的负载,影响集群性能和稳定;可以怎样减少或者避免大量消费延时呢?当topic数据量非常大时,建议一个分区开启一个线程去消费;对topic消费延时添加监控告警,及时发现处理;当topic数据可以丢弃时,遇到超大延时,比如单个分区延迟记录超过千万甚至数亿,那么可以重置topic的消费点位进行紧急处理;【此方案一般在极端场景才使用】避免重置topic的分区offset到很早的位置,这可能造成拉取大量历史数据;1.11.2 Linux服务器参数优化我们需要对Linux的文件句柄、pagecache等参数进行优化。可参考《Linux Page Cache调优在Kafka中的应用》。1.12.硬件优化磁盘优化在条件允许的情况下,可以采用SSD固态硬盘替换HDD机械硬盘,解决机械盘IO性能较低的问题;如果没有SSD固态硬盘,则可以对服务器上的多块硬盘做硬RAID(一般采用RAID10),让broker节点的IO负载更加均衡。如果是HDD机械硬盘,一个broker可以挂载多块硬盘,比如 12块*4TB。内存由于Kafka属于高频读写型服务,而Linux的读写请求基本走的都是Page Cache,所以单节点内存大一些对性能会有比较明显的提升。一般选择256GB或者更高。网络提升网络带宽:在条件允许的情况下,网络带宽越大越好。因为这样网络带宽才不会成为性能瓶颈,最少也要达到万兆网络( 10Gb,网卡为全双工)才能具备相对较高的吞吐量。如果是单通道,网络出流量与入流量之和的上限理论值是1.25GB/s;如果是双工双通道,网络出入流量理论值都可以达到1.25GB/s。网络隔离打标:由于一个机房可能既部署有离线集群(比如HBase、Spark、Hadoop等)又部署有实时集群(如Kafka)。那么实时集群和离线集群挂载到同一个交换机下的服务器将出现竞争网络带宽的问题,离线集群可能对实时集群造成影响。所以我们需要进行交换机层面的隔离,让离线机器和实时集群不要挂载到相同的交换机下。即使有挂载到相同交换机下面的,我们也将进行网络通行优先级(金、银、铜、铁)标记,当网络带宽紧张的时候,让实时业务优先通行。CPUKafka的瓶颈不在CPU,单节点一般有32核的CPU都足够使用。1.13.平台化现在问题来了,前面我们提到很多监控、优化等手段;难道我们管理员或者业务用户对集群所有的操作都需要登录集群服务器吗?答案当然是否定的,我们需要丰富的平台化功能来支持。一方面是为了提升我们操作的效率,另外一方面也是为了提升集群的稳定和降低出错的可能。配置管理黑屏操作,每次修改broker的server.properties配置文件都没有变更记录可追溯,有时可能因为有人修改了集群配置导致一些故障,却找不到相关记录。如果我们把配置管理做到平台上,每次变更都有迹可循,同时降低了变更出错的风险。滚动重启当我们需要做线上变更时,有时候需要对集群对多个节点做滚动重启,如果到命令行去操作,那效率将变得很低,而且需要人工去干预,浪费人力。这个时候我们就需要把这种重复性的工作进行平台化,提升我们的操作效率。集群管理集群管理主要是把原来在命令行的一系列操作做到平台上,用户和管理员不再需要黑屏操作Kafka集群;这样做主要有以下优点:提升操作效率;操作出错概率更小,集群更安全;所有操作有迹可循,可以追溯;集群管理主要包括:broker管理、topic管理、生产/消费权限管理、用户管理等1.13.1 mock功能在平台上为用户的topic提供生产样例数据与消费抽样的功能,用户可以不用自己写代码也可以测试topic是否可以使用,权限是否正常;在平台上为用户的topic提供生产/消费权限验证功能,让用户可以明确自己的账号对某个topic有没有读写权限;1.13.2 权限管理把用户读/写权限管理等相关操作进行平台化。1.13.3 扩容/缩容把broker节点上下线做到平台上,所有的上线和下线节点不再需要操作命令行。1.13.4 集群治理1)无流量topic的治理,对集群中无流量topic进行清理,减少过多无用元数据对集群造成的压力;2)topic分区数据大小治理,把topic分区数据量过大的topic(如单分区数据量超过100GB/天)进行梳理,看看是否需要扩容,避免数据集中在集群部分节点上;3)topic分区数据倾斜治理,避免客户端在生产消息的时候,指定消息的key,但是key过于集中,消息只集中分布在部分分区,导致数据倾斜;4)topic分区分散性治理,让topic分区分布在集群尽可能多的broker上,这样可以避免因topic流量突增,流量只集中到少数节点上的风险,也可以避免某个broker异常对topic影响非常大;5)topic分区消费延时治理;一般有延时消费较多的时候有两种情况,一种是集群性能下降,另外一种是业务方的消费并发度不够,如果是消费者并发不够的话应该与业务联系增加消费并发。1.13.5 监控告警1)把所有指标采集做成平台可配置,提供统一的指标采集和指标展示及告警平台,实现一体化监控;2)把上下游业务进行关联,做成全链路监控;3)用户可以配置topic或者分区流量延时、突变等监控告警;1.13.6 业务大屏业务大屏主要指标:集群个数、节点个数、日入流量大小、日入流量记录、日出流量大小、日出流量记录、每秒入流量大小、每秒入流量记录、每秒出流量大小、每秒出流量记录、用户个数、生产延时、消费延时、数据可靠性、服务可用性、数据存储大小、资源组个数、topic个数、分区个数、副本个数、消费组个数等指标。1.13.7 流量限制把用户流量现在做到平台,在平台进行智能限流处理。1.13.8 负载均衡把自动负载均衡功能做到平台,通过平台进行调度和管理。1.13.9 资源预算当集群达到一定规模,流量不断增长,那么集群扩容机器从哪里来呢?业务的资源预算,让集群里面的多个业务根据自己在集群中当流量去分摊整个集群的硬件成本;当然,独立集群与独立隔离的资源组,预算方式可以单独计算。1.14.性能评估1.14.1 单broker性能评估我们做单broker性能评估的目的包括以下几方面:1)为我们进行资源申请评估提供依据;2)让我们更了解集群的读写能力及瓶颈在哪里,针对瓶颈进行优化;3)为我们限流阈值设置提供依据;4)为我们评估什么时候应该扩容提供依据;1.14.2 topic分区性能评估1)为我们创建topic时,评估应该指定多少分区合理提供依据;2)为我们topic的分区扩容评估提供依据;1.14.3 单磁盘性能评估1)为我们了解磁盘的真正读写能力,为我们选择更合适Kafka的磁盘类型提供依据;2)为我们做磁盘流量告警阈值设置提供依据;1.14.4 集群规模限制摸底1)我们需要了解单个集群规模的上限或者是元数据规模的上限,探索相关信息对集群性能和稳定性的影响;2)根据摸底情况,评估集群节点规模的合理范围,及时预测风险,进行超大集群的拆分等工作;1.15 DNS+LVS的网络架构当我们的集群节点达到一定规模,比如单集群数百个broker节点,那么此时我们生产消费客户端指定bootstrap.servers配置时,如果指定呢?是随便选择其中几个broker配置还是全部都配上呢?其实以上做法都不合适,如果只配置几个IP,当我们配置当几个broker节点下线后,我们当应用将无法连接到Kafka集群;如果配置所有IP,那更不现实啦,几百个IP,那么我们应该怎么做呢?方案:采用DNS+LVS网络架构,最终生产者和消费者客户端只需要配置域名就可以啦。需要注意的是,有新节点加入集群时,需要添加映射;有节点下线时,需要从映射中踢掉,否则这批机器如果拿到其他的地方去使用,如果端口和Kafka的一样的话,原来集群部分请求将发送到这个已经下线的服务器上来,造成生产环境重点故障。
Kafka万亿级消息实战
原创 | Java 2021 超神之路,很肝~中文详细注释的开源项目RPC 框架 Dubbo 源码解析网络应用框架 Netty 源码解析消息中间件 RocketMQ 源码解析数据库中间件 Sharding-JDBC 和 MyCAT 源码解析作业调度中间件 Elastic-Job 源码解析分布式事务中间件 TCC-Transaction 源码解析Eureka 和 Hystrix 源码解析Java 并发源码一、Kafka应用二、开源版本功能缺陷三、kafka发展趋势四、如何贡献社区一、Kafka应用本文主要总结当Kafka集群流量达到 万亿级记录/天或者十万亿级记录/天 甚至更高后,我们需要具备哪些能力才能保障集群高可用、高可靠、高性能、高吞吐、安全的运行。这里总结内容主要针对Kafka2.1.1版本,包括集群版本升级、数据迁移、流量限制、监控告警、负载均衡、集群扩/缩容、资源隔离、集群容灾、集群安全、性能优化、平台化、开源版本缺陷、社区动态等方面。本文主要是介绍核心脉络,不做过多细节讲解。下面我们先来看看Kafka作为数据中枢的一些核心应用场景。下图展示了一些主流的数据处理流程,Kafka起到一个数据中枢的作用。接下来看看我们Kafka平台整体架构;1.1 版本升级1.1.1 开源版本如何进行版本滚动升级与回退官网地址:http://kafka.apache.org1.1.2 源码改造如何升级与回退由于在升级过程中,必然出现新旧代码逻辑交替的情况。集群内部部分节点是开源版本,另外一部分节点是改造后的版本。所以,需要考虑在升级过程中,新旧代码混合的情况,如何兼容以及出现故障时如何回退。1.2 数据迁移由于Kafka集群的架构特点,这必然导致集群内流量负载不均衡的情况,所以我们需要做一些数据迁移来实现集群不同节点间的流量均衡。Kafka开源版本为数据迁移提供了一个脚本工具“bin/kafka-reassign-partitions.sh ”,如果自己没有实现自动负载均衡,可以使用此脚本。开源版本提供的这个脚本生成迁移计划完全是人工干预的,当集群规模非常大时,迁移效率变得非常低下,一般以天为单位进行计算。当然,我们可以实现一套自动化的均衡程序,当负载均衡实现自动化以后,基本使用调用内部提供的API,由程序去帮我们生成迁移计划及执行迁移任务。需要注意的是,迁移计划有指定数据目录和不指定数据目录两种,指定数据目录的需要配置ACL安全认证。官网地址:http://kafka.apache.org1.2.1 broker间数据迁移不指定数据目录//未指定迁移目录的迁移计划 "version":1, "partitions":[ {"topic":"yyj4","partition":0,"replicas":[1000003,1000004]}, {"topic":"yyj4","partition":1,"replicas":[1000003,1000004]}, {"topic":"yyj4","partition":2,"replicas":[1000003,1000004]} }指定数据目录//指定迁移目录的迁移计划 "version":1, "partitions":[ {"topic":"yyj1","partition":0,"replicas":[1000006,1000005],"log_dirs":["/data1/bigdata/mydata1","/data1/bigdata/mydata3"]}, {"topic":"yyj1","partition":1,"replicas":[1000006,1000005],"log_dirs":["/data1/bigdata/mydata1","/data1/bigdata/mydata3"]}, {"topic":"yyj1","partition":2,"replicas":[1000006,1000005],"log_dirs":["/data1/bigdata/mydata1","/data1/bigdata/mydata3"]} }1.2.2 broker内部磁盘间数据迁移生产环境的服务器一般都是挂载多块硬盘,比如4块/12块等;那么可能出现在Kafka集群内部,各broker间流量比较均衡,但是在broker内部,各磁盘间流量不均衡,导致部分磁盘过载,从而影响集群性能和稳定,也没有较好的利用硬件资源。在这种情况下,我们就需要对broker内部多块磁盘的流量做负载均衡,让流量更均匀的分布到各磁盘上。1.2.3 并发数据迁移当前Kafka开源版本(2.1.1版本)提供的副本迁移工具“bin/kafka-reassign-partitions.sh”在同一个集群内只能实现迁移任务的串行。对于集群内已经实现多个资源组物理隔离的情况,由于各资源组不会相互影响,但是却不能友好的进行并行的提交迁移任务,迁移效率有点低下,这种不足直到2.6.0版本才得以解决。如果需要实现并发数据迁移,可以选择升级Kafka版本或者修改Kafka源码。1.2.4 终止数据迁移当前Kafka开源版本(2.1.1版本)提供的副本迁移工具“bin/kafka-reassign-partitions.sh”在启动迁移任务后,无法终止迁移。当迁移任务对集群的稳定性或者性能有影响时,将变得束手无策,只能等待迁移任务执行完毕(成功或者失败),这种不足直到2.6.0版本才得以解决。如果需要实现终止数据迁移,可以选择升级Kafka版本或者修改Kafka源码。1.3 流量限制1.3.1 生产消费流量限制经常会出现一些突发的,不可预测的异常生产或者消费流量会对集群的IO等资源产生巨大压力,最终影响整个集群的稳定与性能。那么我们可以对用户的生产、消费、副本间数据同步进行流量限制,这个限流机制并不是为了限制用户,而是避免突发的流量影响集群的稳定和性能,给用户可以更好的服务。如下图所示,节点入流量由140MB/s左右突增到250MB/s,而出流量则从400MB/s左右突增至800MB/s。如果没有限流机制,那么集群的多个节点将有被这些异常流量打挂的风险,甚至造成集群雪崩。图片生产/消费流量限制官网地址:点击链接对于生产者和消费者的流量限制,官网提供了以下几种维度组合进行限制(当然,下面限流机制存在一定缺陷,后面在“Kafka开源版本功能缺陷”我们将提到):/config/users/<user>/clients/<client-id> //根据用户和客户端ID组合限流 /config/users/<user>/clients/<default> /config/users/<user>//根据用户限流 这种限流方式是我们最常用的方式 /config/users/<default>/clients/<client-id> /config/users/<default>/clients/<default> /config/users/<default> /config/clients/<client-id> /config/clients/<default>在启动Kafka的broker服务时需要开启JMX参数配置,方便通过其他应用程序采集Kafka的各项JMX指标进行服务监控。当用户需要调整限流阈值时,根据单个broker所能承受的流量进行智能评估,无需人工干预判断是否可以调整;对于用户流量限制,主要需要参考的指标包括以下两个:“(1)消费流量指标:ObjectName:kafka.server:type=Fetch,user=acl认证用户名称 属性:byte-rate(用户在当前broker的出流量)、throttle-time(用户在当前broker的出流量被限制时间)(2)生产流量指标:ObjectName:kafka.server:type=Produce,user=acl认证用户名称 属性:byte-rate(用户在当前broker的入流量)、throttle-time(用户在当前broker的入流量被限制时间)1.3.2 follower同步leader/数据迁移流量限制副本迁移/数据同步流量限制官网地址:链接涉及参数如下://副本同步限流配置共涉及以下4个参数 leader.replication.throttled.rate follower.replication.throttled.rate leader.replication.throttled.replicas follower.replication.throttled.replicas辅助指标如下:(1)副本同步出流量指标:ObjectName:kafka.server:type=BrokerTopicMetrics,name=ReplicationBytesOutPerSec (2)副本同步入流量指标:ObjectName:kafka.server:type=BrokerTopicMetrics,name=ReplicationBytesInPerSec1.4 监控告警关于Kafka的监控有一些开源的工具可用使用,比如下面这几种:“Kafka Manager;Kafka Eagle;Kafka Monitor;KafkaOffsetMonitor;我们已经把Kafka Manager作为我们查看一些基本指标的工具嵌入平台,然而这些开源工具不能很好的融入到我们自己的业务系统或者平台上。所以,我们需要自己去实现一套粒度更细、监控更智能、告警更精准的系统。其监控覆盖范围应该包括基础硬件、操作系统(操作系统偶尔出现系统进程hang住情况,导致broker假死,无法正常提供服务)、Kafka的broker服务、Kafka客户端应用程序、zookeeper集群、上下游全链路监控。1.4.1 硬件监控网络监控:核心指标包括网络入流量、网络出流量、网络丢包、网络重传、处于TIME.WAIT的TCP连接数、交换机、机房带宽、DNS服务器监控(如果DNS服务器异常,可能出现流量黑洞,引起大面积业务故障)等。磁盘监控:核心指标包括监控磁盘write、磁盘read(如果消费时没有延时,或者只有少量延时,一般都没有磁盘read操作)、磁盘ioutil、磁盘iowait(这个指标如果过高说明磁盘负载较大)、磁盘存储空间、磁盘坏盘、磁盘坏块/坏道(坏道或者坏块将导致broker处于半死不活状态,由于有crc校验,消费者将被卡住)等。CPU监控:监控CPU空闲率/负载,主板故障等,通常CPU使用率比较低不是Kafka的瓶颈。内存/交换区监控:内存使用率,内存故障。一般情况下,服务器上除了启动Kafka的broker时分配的堆内存以外,其他内存基本全部被用来做PageCache。缓存命中率监控:由于是否读磁盘对Kafka的性能影响很大,所以我们需要监控Linux的PageCache缓存命中率,如果缓存命中率高,则说明消费者基本命中缓存。系统日志:我们需要对操作系统的错误日志进行监控告警,及时发现一些硬件故障。1.4.2 broker服务监控broker服务的监控,主要是通过在broker服务启动时指定JMX端口,然后通过实现一套指标采集程序去采集JMX指标。(服务端指标官网地址)broker级监控: broker进程、broker入流量字节大小/记录数、broker出流量字节大小/记录数、副本同步入流量、副本同步出流量、broker间流量偏差、broker连接数、broker请求队列数、broker网络空闲率、broker生产延时、broker消费延时、broker生产请求数、broker消费请求数、broker上分布leader个数、broker上分布副本个数、broker上各磁盘流量、broker GC等。topic级监控: topic入流量字节大小/记录数、topic出流量字节大小/记录数、无流量topic、topic流量突变(突增/突降)、topic消费延时。partition级监控: 分区入流量字节大小/记录数、分区出流量字节大小/记录数、topic分区副本缺失、分区消费延迟记录、分区leader切换、分区数据倾斜(生产消息时,如果指定了消息的key容易造成数据倾斜,这严重影响Kafka的服务性能)、分区存储大小(可以治理单分区过大的topic)。用户级监控: 用户出/入流量字节大小、用户出/入流量被限制时间、用户流量突变(突增/突降)。broker服务日志监控: 对server端打印的错误日志进行监控告警,及时发现服务异常。1.4.3 客户端监控客户端监控主要是自己实现一套指标上报程序,这个程序需要实现org.apache.kafka.common.metrics.MetricsReporter 接口。然后在生产者或者消费者的配置中加入配置项 metric.reporters,如下所示:Properties props = new Properties(); props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, ""); props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class.getName()); props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); //ClientMetricsReporter类实现org.apache.kafka.common.metrics.MetricsReporter接口 props.put(ProducerConfig.METRIC_REPORTER_CLASSES_CONFIG, ClientMetricsReporter.class.getName()); ...客户端指标官网地址:http://kafka.apache.org/21/documentation.html#selector_monitoringhttp://kafka.apache.org/21/documentation.html#common_node_monitoringhttp://kafka.apache.org/21/documentation.html#producer_monitoringhttp://kafka.apache.org/21/documentation.html#producer_sender_monitoringhttp://kafka.apache.org/21/documentation.html#consumer_monitoringhttp://kafka.apache.org/21/documentation.html#consumer_fetch_monitoring客户端监控流程架构如下图所示:1.4.3.1 生产者客户端监控“维度:用户名称、客户端ID、客户端IP、topic名称、集群名称、brokerIP;指标:连接数、IO等待时间、生产流量大小、生产记录数、请求次数、请求延时、发送错误/重试次数等。1.4.3.2 消费者客户端监控“维度:用户名称、客户端ID、客户端IP、topic名称、集群名称、消费组、brokerIP、topic分区;指标:连接数、io等待时间、消费流量大小、消费记录数、消费延时、topic分区消费延迟记录等。1.4.4 Zookeeper监控“\1) Zookeeper进程监控;\2) Zookeeper的leader切换监控;\3) Zookeeper服务的错误日志监控;1.4.5 全链路监控当数据链路非常长的时候(比如:业务应用->埋点SDk->数据采集->Kafka->实时计算->业务应用),我们定位问题通常需要经过多个团队反复沟通与排查才能发现问题到底出现在哪个环节,这样排查问题效率比较低下。在这种情况下,我们就需要与上下游一起梳理整个链路的监控。出现问题时,第一时间定位问题出现在哪个环节,缩短问题定位与故障恢复时间。1.5 资源隔离1.5.1 相同集群不同业务资源物理隔离我们对所有集群中不同对业务进行资源组物理隔离,避免各业务之间相互影响。在这里,我们假设集群有4个broker节点(Broker1/Broker2/Broker3/Broker4),2个业务(业务A/业务B),他们分别拥有topic分区分布如下图所示,两个业务topic都分散在集群的各个broker上,并且在磁盘层面也存在交叉。试想一下,如果我们其中一个业务异常,比如流量突增,导致broker节点异常或者被打挂。那么这时候另外一个业务也将受到影响,这样将大大的影响了我们服务的可用性,造成故障,扩大了故障影响范围。针对这些痛点,我们可以对集群中的业务进行物理资源隔离,各业务独享资源,进行资源组划分(这里把4各broker划分为Group1和Group2两个资源组)如下图所示,不同业务的topic分布在自己的资源组内,当其中一个业务异常时,不会波及另外一个业务,这样就可以有效的缩小我们的故障范围,提高服务可用性。1.6 集群归类我们把集群根据业务特点进行拆分为日志集群、监控集群、计费集群、搜索集群、离线集群、在线集群等,不同场景业务放在不同集群,避免不同业务相互影响。1.7 扩容/缩容1.7.1 topic扩容分区随着topic数据量增长,我们最初创建的topic指定的分区个数可能已经无法满足数量流量要求,所以我们需要对topic的分区进行扩展。扩容分区时需要考虑一下几点:“必须保证topic分区leader与follower轮询的分布在资源组内所有broker上,让流量分布更加均衡,同时需要考虑相同分区不同副本跨机架分布以提高容灾能力;“当topic分区leader个数除以资源组节点个数有余数时,需要把余数分区leader优先考虑放入流量较低的broker。1.7.2 broker上线随着业务量增多,数据量不断增大,我们的集群也需要进行broker节点扩容。关于扩容,我们需要实现以下几点:“扩容智能评估:根据集群负载,把是否需要扩容评估程序化、智能化;“智能扩容:当评估需要扩容后,把扩容流程以及流量均衡平台化。1.7.3 broker下线某些场景下,我们需要下线我们的broker,主要包括以下几个场景:“一些老化的服务器需要下线,实现节点下线平台化;“服务器故障,broker故障无法恢复,我们需要下线故障服务器,实现节点下线平台化;“有更优配置的服务器替换已有broker节点,实现下线节点平台化。1.8 负载均衡我们为什么需要负载均衡呢?首先,我们来看第一张图,下图是我们集群某个资源组刚扩容后的流量分布情况,流量无法自动的分摊到我们新扩容后的节点上。那么这个时候需要我们手动去触发数据迁移,把部分副本迁移至新节点上才能实现流量均衡。下面,我们来看一下第二张图。这张图我们可以看出流量分布非常不均衡,最低和最高流量偏差数倍以上。这和Kafka的架构特点有关,当集群规模与数据量达到一定量后,必然出现当问题。这种情况下,我们也需要进行负载均衡。我们再来看看第三张图。这里我们可以看出出流量只有部分节点突增,这就是topic分区在集群内部不够分散,集中分布到了某几个broker导致,这种情况我们也需要进行扩容分区和均衡。我们比较理想的流量分布应该如下图所示,各节点间流量偏差非常小,这种情况下,既可以增强集群扛住流量异常突增的能力又可以提升集群整体资源利用率和服务稳定性,降低成本。负载均衡我们需要实现以下效果:“1)生成副本迁移计划以及执行迁移任务平台化、自动化、智能化;2)执行均衡后broker间流量比较均匀,且单个topic分区均匀分布在所有broker节点上;3)执行均衡后broker内部多块磁盘间流量比较均衡;要实现这个效果,我们需要开发一套自己的负载均衡工具,如对开源的 cruise control进行二次开发;此工具的核心主要在生成迁移计划的策略,迁移计划的生成方案直接影响到最后集群负载均衡的效果。参考内容:linkedIn/cruise-controlIntroduction to Kafka Cruise ControlCloudera Cruise Control REST API Referencecruise control架构图如下:在生成迁移计划时,我们需要考虑以下几点:“1)选择核心指标作为生成迁移计划的依据,比如出流量、入流量、机架、单topic分区分散性等;“2)优化用来生成迁移计划的指标样本,比如过滤流量突增/突降/掉零等异常样本;“3)各资源组的迁移计划需要使用的样本全部为资源组内部样本,不涉及其他资源组,无交叉;“4)治理单分区过大topic,让topic分区分布更分散,流量不集中在部分broker,让topic单分区数据量更小,这样可以减少迁移的数据量,提升迁移速度;“5)已经均匀分散在资源组内的topic,加入迁移黑名单,不做迁移,这样可以减少迁移的数据量,提升迁移速度;“6)做topic治理,排除长期无流量topic对均衡的干扰;“7)新建topic或者topic分区扩容时,应让所有分区轮询分布在所有broker节点,轮询后余数分区优先分布流量较低的broker;“8)扩容broker节点后开启负载均衡时,优先把同一broker分配了同一大流量(流量大而不是存储空间大,这里可以认为是每秒的吞吐量)topic多个分区leader的,迁移一部分到新broker节点;“9)提交迁移任务时,同一批迁移计划中的分区数据大小偏差应该尽可能小,这样可以避免迁移任务中小分区迁移完成后长时间等待大分区的迁移,造成任务倾斜;1.9 安全认证是不是我们的集群所有人都可以随意访问呢?当然不是,为了集群的安全,我们需要进行权限认证,屏蔽非法操作。主要包括以下几个方面需要做安全认证:“(1)生产者权限认证;(2)消费者权限认证;(3)指定数据目录迁移安全认证;官网地址:http://kafka.apache.org1.10 集群容灾跨机架容灾:官网地址:http://kafka.apache.org跨集群/机房容灾: 如果有异地双活等业务场景时,可以参考Kafka2.7版本的MirrorMaker 2.0。“GitHub地址:https://github.com精确KIP地址 :https://cwiki.apache.orgZooKeeper集群上Kafka元数据恢复: 我们会定期对ZooKeeper上的权限信息数据做备份处理,当集群元数据异常时用于恢复。1.11 参数/配置优化broker服务参数优化: 这里我只列举部分影响性能的核心参数。num.network.threads #创建Processor处理网络请求线程个数,建议设置为broker当CPU核心数*2,这个值太低经常出现网络空闲太低而缺失副本。 num.io.threads #创建KafkaRequestHandler处理具体请求线程个数,建议设置为broker磁盘个数*2 num.replica.fetchers #建议设置为CPU核心数/4,适当提高可以提升CPU利用率及follower同步leader数据当并行度。 compression.type #建议采用lz4压缩类型,压缩可以提升CPU利用率同时可以减少网络传输数据量。 queued.max.requests #如果是生产环境,建议配置最少500以上,默认为500。 log.flush.scheduler.interval.ms log.flush.interval.ms log.flush.interval.messages #这几个参数表示日志数据刷新到磁盘的策略,应该保持默认配置,刷盘策略让操作系统去完成,由操作系统来决定什么时候把数据刷盘; #如果设置来这个参数,可能对吞吐量影响非常大; auto.leader.rebalance.enable #表示是否开启leader自动负载均衡,默认true;我们应该把这个参数设置为false,因为自动负载均衡不可控,可能影响集群性能和稳定;生产优化: 这里我只列举部分影响性能的核心参数。linger.ms #客户端生产消息等待多久时间才发送到服务端,单位:毫秒。和batch.size参数配合使用;适当调大可以提升吞吐量,但是如果客户端如果down机有丢失数据风险; batch.size #客户端发送到服务端消息批次大小,和linger.ms参数配合使用;适当调大可以提升吞吐量,但是如果客户端如果down机有丢失数据风险; compression.type #建议采用lz4压缩类型,具备较高的压缩比及吞吐量;由于Kafka对CPU的要求并不高,所以,可以通过压缩,充分利用CPU资源以提升网络吞吐量; buffer.memory #客户端缓冲区大小,如果topic比较大,且内存比较充足,可以适当调高这个参数,默认只为33554432(32MB) retries #生产失败后的重试次数,默认0,可以适当增加。当重试超过一定次数后,如果业务要求数据准确性较高,建议做容错处理。 retry.backoff.ms #生产失败后,重试时间间隔,默认100ms,建议不要设置太大或者太小。除了一些核心参数优化外,我们还需要考虑比如topic的分区个数和topic保留时间;如果分区个数太少,保留时间太长,但是写入数据量非常大的话,可能造成以下问题:“1)topic分区集中落在某几个broker节点上,导致流量副本失衡;“2)导致broker节点内部某几块磁盘读写超负载,存储被写爆;1.11.1 消费优化消费最大的问题,并且经常出现的问题就是消费延时,拉历史数据。当大量拉取历史数据时将出现大量读盘操作,污染pagecache,这个将加重磁盘的负载,影响集群性能和稳定;可以怎样减少或者避免大量消费延时呢?“当topic数据量非常大时,建议一个分区开启一个线程去消费;对topic消费延时添加监控告警,及时发现处理;当topic数据可以丢弃时,遇到超大延时,比如单个分区延迟记录超过千万甚至数亿,那么可以重置topic的消费点位进行紧急处理;【此方案一般在极端场景才使用】避免重置topic的分区offset到很早的位置,这可能造成拉取大量历史数据;1.11.2 Linux服务器参数优化我们需要对Linux的文件句柄、pagecache等参数进行优化。1.12.硬件优化磁盘优化在条件允许的情况下,可以采用SSD固态硬盘替换HDD机械硬盘,解决机械盘IO性能较低的问题;如果没有SSD固态硬盘,则可以对服务器上的多块硬盘做硬RAID(一般采用RAID10),让broker节点的IO负载更加均衡。如果是HDD机械硬盘,一个broker可以挂载多块硬盘,比如 12块*4TB。内存由于Kafka属于高频读写型服务,而Linux的读写请求基本走的都是Page Cache,所以单节点内存大一些对性能会有比较明显的提升。一般选择256GB或者更高。网络提升网络带宽:在条件允许的情况下,网络带宽越大越好。因为这样网络带宽才不会成为性能瓶颈,最少也要达到万兆网络( 10Gb,网卡为全双工)才能具备相对较高的吞吐量。如果是单通道,网络出流量与入流量之和的上限理论值是1.25GB/s;如果是双工双通道,网络出入流量理论值都可以达到1.25GB/s。网络隔离打标:由于一个机房可能既部署有离线集群(比如HBase、Spark、Hadoop等)又部署有实时集群(如Kafka)。那么实时集群和离线集群挂载到同一个交换机下的服务器将出现竞争网络带宽的问题,离线集群可能对实时集群造成影响。所以我们需要进行交换机层面的隔离,让离线机器和实时集群不要挂载到相同的交换机下。即使有挂载到相同交换机下面的,我们也将进行网络通行优先级(金、银、铜、铁)标记,当网络带宽紧张的时候,让实时业务优先通行。CPUKafka的瓶颈不在CPU,单节点一般有32核的CPU都足够使用。1.13 平台化现在问题来了,前面我们提到很多监控、优化等手段;难道我们管理员或者业务用户对集群所有的操作都需要登录集群服务器吗?答案当然是否定的,我们需要丰富的平台化功能来支持。一方面是为了提升我们操作的效率,另外一方面也是为了提升集群的稳定和降低出错的可能。配置管理黑屏操作,每次修改broker的server.properties配置文件都没有变更记录可追溯,有时可能因为有人修改了集群配置导致一些故障,却找不到相关记录。如果我们把配置管理做到平台上,每次变更都有迹可循,同时降低了变更出错的风险。滚动重启当我们需要做线上变更时,有时候需要对集群对多个节点做滚动重启,如果到命令行去操作,那效率将变得很低,而且需要人工去干预,浪费人力。这个时候我们就需要把这种重复性的工作进行平台化,提升我们的操作效率。集群管理集群管理主要是把原来在命令行的一系列操作做到平台上,用户和管理员不再需要黑屏操作Kafka集群;这样做主要有以下优点:“提升操作效率;操作出错概率更小,集群更安全;所有操作有迹可循,可以追溯;集群管理主要包括:broker管理、topic管理、生产/消费权限管理、用户管理等1.13.1 mock功能“在平台上为用户的topic提供生产样例数据与消费抽样的功能,用户可以不用自己写代码也可以测试topic是否可以使用,权限是否正常;“在平台上为用户的topic提供生产/消费权限验证功能,让用户可以明确自己的账号对某个topic有没有读写权限;1.13.2 权限管理把用户读/写权限管理等相关操作进行平台化。1.13.3 扩容/缩容把broker节点上下线做到平台上,所有的上线和下线节点不再需要操作命令行。1.13.4 集群治理“1)无流量topic的治理,对集群中无流量topic进行清理,减少过多无用元数据对集群造成的压力;“2)topic分区数据大小治理,把topic分区数据量过大的topic(如单分区数据量超过100GB/天)进行梳理,看看是否需要扩容,避免数据集中在集群部分节点上;“3)topic分区数据倾斜治理,避免客户端在生产消息的时候,指定消息的key,但是key过于集中,消息只集中分布在部分分区,导致数据倾斜;“4)topic分区分散性治理,让topic分区分布在集群尽可能多的broker上,这样可以避免因topic流量突增,流量只集中到少数节点上的风险,也可以避免某个broker异常对topic影响非常大;“5)topic分区消费延时治理;一般有延时消费较多的时候有两种情况,一种是集群性能下降,另外一种是业务方的消费并发度不够,如果是消费者并发不够的化应该与业务联系增加消费并发。1.13.5 监控告警“1)把所有指标采集做成平台可配置,提供统一的指标采集和指标展示及告警平台,实现一体化监控;2)把上下游业务进行关联,做成全链路监控;3)用户可以配置topic或者分区流量延时、突变等监控告警;1.13.6 业务大屏业务大屏主要指标:集群个数、节点个数、日入流量大小、日入流量记录、日出流量大小、日出流量记录、每秒入流量大小、每秒入流量记录、每秒出流量大小、每秒出流量记录、用户个数、生产延时、消费延时、数据可靠性、服务可用性、数据存储大小、资源组个数、topic个数、分区个数、副本个数、消费组个数等指标。1.13.7 流量限制把用户流量现在做到平台,在平台进行智能限流处理。1.13.8 负载均衡把自动负载均衡功能做到平台,通过平台进行调度和管理。1.13.9 资源预算当集群达到一定规模,流量不断增长,那么集群扩容机器从哪里来呢?业务的资源预算,让集群里面的多个业务根据自己在集群中当流量去分摊整个集群的硬件成本;当然,独立集群与独立隔离的资源组,预算方式可以单独计算。1.14.性能评估1.14.1 单broker性能评估我们做单broker性能评估的目的包括以下几方面:“1)为我们进行资源申请评估提供依据;2)让我们更了解集群的读写能力及瓶颈在哪里,针对瓶颈进行优化;3)为我们限流阈值设置提供依据;4)为我们评估什么时候应该扩容提供依据;1.14.2 topic分区性能评估“1)为我们创建topic时,评估应该指定多少分区合理提供依据;2)为我们topic的分区扩容评估提供依据;1.14.3 单磁盘性能评估“1)为我们了解磁盘的真正读写能力,为我们选择更合适Kafka的磁盘类型提供依据;2)为我们做磁盘流量告警阈值设置提供依据;1.14.4 集群规模限制摸底“1)我们需要了解单个集群规模的上限或者是元数据规模的上限,探索相关信息对集群性能和稳定性的影响;2)根据摸底情况,评估集群节点规模的合理范围,及时预测风险,进行超大集群的拆分等工作;1.15 DNS+LVS的网络架构当我们的集群节点达到一定规模,比如单集群数百个broker节点,那么此时我们生产消费客户端指定bootstrap.servers配置时,如果指定呢?是随便选择其中几个broker配置还是全部都配上呢?其实以上做法都不合适,如果只配置几个IP,当我们配置当几个broker节点下线后,我们当应用将无法连接到Kafka集群;如果配置所有IP,那更不现实啦,几百个IP,那么我们应该怎么做呢?方案: 采用DNS+LVS网络架构,最终生产者和消费者客户端只需要配置域名就可以啦。需要注意的是,有新节点加入集群时,需要添加映射;有节点下线时,需要从映射中踢掉,否则这批机器如果拿到其他的地方去使用,如果端口和Kafka的一样的话,原来集群部分请求将发送到这个已经下线的服务器上来,造成生产环境重点故障。基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能项目地址:https://github.com/YunaiV/ruoyi-vue-pro视频教程:https://doc.iocoder.cn/video/二、开源版本功能缺陷RTMP协议主要的特点有:多路复用,分包和应用层协议。以下将对这些特点进行详细的描述。2.1 副本迁移“无法实现增量迁移;【我们已经基于2.1.1版本源码改造,实现了增量迁移】“无法实现并发迁移;【开源版本直到2.6.0才实现了并发迁移】“无法实现终止迁移;【我们已经基于2.1.1版本源码改造,实现了终止副本迁移】【开源版本直到2.6.0才实现了暂停迁移,和终止迁移有些不一样,不会回滚元数据】“当指定迁移数据目录时,迁移过程中,如果把topic保留时间改短,topic保留时间针对正在迁移topic分区不生效,topic分区过期数据无法删除;【开源版本bug,目前还没有修复】“当指定迁移数据目录时,当迁移计划为以下场景时,整个迁移任务无法完成迁移,一直处于卡死状态;【开源版本bug,目前还没有修复】“迁移过程中,如果有重启broker节点,那个broker节点上的所有leader分区无法切换回来,导致节点流量全部转移到其他节点,直到所有副本被迁移完毕后leader才会切换回来;【开源版本bug,目前还没有修复】。在原生的Kafka版本中存在以下指定数据目录场景无法迁移完毕的情况,此版本我们也不决定修复次bug:1.针对同一个topic分区,如果部分目标副本相比原副本是所属broker发生变化,部分目标副本相比原副本是broker内部所属数据目录发生变化; 那么副本所属broker发生变化的那个目标副本可以正常迁移完毕,目标副本是在broker内部数据目录发生变化的无法正常完成迁移; 但是旧副本依然可以正常提供生产、消费服务,并且不影响下一次迁移任务的提交,下一次迁移任务只需要把此topic分区的副本列表所属broker列表变更后提交依然可以正常完成迁移,并且可以清理掉之前未完成的目标副本; 这里假设topic yyj1的初始化副本分布情况如下: "version":1, "partitions":[ {"topic":"yyj","partition":0,"replicas":[1000003,1000001],"log_dirs":["/kfk211data/data31","/kfk211data/data13"]} //迁移场景1: "version":1, "partitions":[ {"topic":"yyj","partition":0,"replicas":[1000003,1000002],"log_dirs":["/kfk211data/data32","/kfk211data/data23"]} //迁移场景2: "version":1, "partitions":[ {"topic":"yyj","partition":0,"replicas":[1000002,1000001],"log_dirs":["/kfk211data/data22","/kfk211data/data13"]} 针对上述的topic yyj1的分布分布情况,此时如果我们的迁移计划为“迁移场景1”或迁移场景2“,那么都将出现有副本无法迁移完毕的情况。 但是这并不影响旧副本处理生产、消费请求,并且我们可以正常提交其他的迁移任务。 为了清理旧的未迁移完成的副本,我们只需要修改一次迁移计划【新的目标副本列表和当前分区已分配副本列表完全不同即可】,再次提交迁移即可。 这里,我们依然以上述的例子做迁移计划修改如下: "version":1, "partitions":[ {"topic":"yyj","partition":0,"replicas":[1000004,1000005],"log_dirs":["/kfk211data/data42","/kfk211data/data53"]} 这样我们就可以正常完成迁移。2.2 流量协议限流粒度较粗,不够灵活精准,不够智能。当前限流维度组合/config/users/<user>/clients/<client-id> /config/users/<user>/clients/<default> /config/users/<user> /config/users/<default>/clients/<client-id> /config/users/<default>/clients/<default> /config/users/<default> /config/clients/<client-id> /config/clients/<default>存在问题当同一个broker上有多个用户同时进行大量的生产和消费时,想要让broker可以正常运行,那必须在做限流时让所有的用户流量阈值之和不超过broker的吞吐上限;如果超过broker上限,那么broker就存在被打挂的风险;然而,即使用户流量没有达到broker的流量上限,但是,如果所有用户流量集中到了某几块盘上,超过了磁盘的读写负载,也会导致所有生产、消费请求将被阻塞,broker可能处于假死状态。解决方案“(1)改造源码,实现单个broker流量上限限制,只要流量达到broker上限立即进行限流处理,所有往这个broker写的用户都可以被限制住;或者对用户进行优先级处理,放过高优先级的,限制低优先级的;“(2)改造源码,实现broker上单块磁盘流量上限限制(很多时候都是流量集中到某几块磁盘上,导致没有达到broker流量上限却超过了单磁盘读写能力上限),只要磁盘流量达到上限,立即进行限流处理,所有往这个磁盘写的用户都可以被限制住;或者对用户进行优先级处理,放过高优先级的,限制低优先级的;“(3)改造源码,实现topic维度限流以及对topic分区的禁写功能;“(4)改造源码,实现用户、broker、磁盘、topic等维度组合精准限流;基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能项目地址:https://github.com/YunaiV/yudao-cloud视频教程:https://doc.iocoder.cn/video/三、kafka发展趋势3.1 Kafka社区迭代计划3.2 逐步弃用ZooKeeper(KIP-500)3.3 controller与broker分离,引入raft协议作为controller的仲裁机制(KIP-630)3.4 分层存储(KIP-405)3.5 可以减少topic分区(KIP-694)3.6 MirrorMaker2精确一次(KIP-656)3.7 下载与各版本特性说明3.8 Kafka所有KIP地址四、如何贡献社区4.1 哪些点可以贡献http://kafka.apache.org/contributing4.2 wiki贡献地址https://cwiki.apache.org/confluence/dashboard.action#all-updates4.3 issues地址1)https://issues.apache.org/jira/projects/KAFKA/issues/KAFKA-10444?filter=allopenissues2)https://issues.apache.org/jira/secure/BrowseProjects.jspa?selectedCategory=all4.4 主要committershttp://kafka.apache.org/committers
Kubeadm 快速搭建 k8s v1.24.1 集群(openEuler 22.03 LTS)
kubeadm 简介kubeadm 是 Kubernetes(以下简称 k8s)官方提供的用于快速安装部署 k8s 集群的工具,伴随 k8s 每个版本的发布都会同步更新,kubeadm 会对集群配置方面的一些实践做调整,通过实验 kubeadm 可以学习到 k8s 官方在集群配置上一些新的最佳实践。这个工具能通过两条指令完成一个 k8s 集群的部署:# 创建一个 Master 节点 $ kubeadm init # 将一个 Node 节点加入到当前集群中 $ kubeadm join <Master节点的IP和端口 >使用 kubeadm 工具大大简化了 k8s 的部署操作。k8s 的部署方式通常使用 kubeadm 部署和二进制方式部署,他们之间的区别:kubeadm 方式部署,组件容器化部署,只有 kubelet 没有被容器化二进制方式部署,传统的守护进程(systemd)管理服务 systemctl实现目标基于 华为 openEuler 22.03 LTS 系统,使用 kubeadm v1.24.1 搭建一套由单 Master node 和两个 Worker node 组成的 k8s v1.24.1 版本的集群环境。系统环境准备部署要求在开始部署之前,部署 k8s 集群的宿主机(vm 或 物理机)需要满足以下几个条件:宿主机 3 台,操作系统 CentOS7.x/8.x-86_x64 系列(此处使用 openEuler 22.03 LTS)硬件配置:RAM 4GB或更多,CPU 核数 2c 或更多,硬盘 60GB 或更多集群中所有机器之间网络互通可以访问外网,需要拉取镜像关闭防火墙,禁止 swap 分区所有集群节点同步系统时间(使用 ntpdate 工具)注意:以上部署规格的配置要求为最小化集群规模要求,生产环境的集群部署要求按实际情况扩展配置,为了保障集群环境的高可用性,搭建集群环境的宿主机通常以奇数( ≥ 3、5、7...)节点最佳。部署规划此处以单 master node 和两 worker node 集群模式为例,使用 kubeadm 部署 Kubernetes v1.24.1 版本集群环境。1. 单 master 集群模式2. vm 资源编排此处使用 VMware Workstation Pro v16.2.2 虚拟机搭建 vm 环境,规划如下:k8s 集群角色ip 地址hostname 主机名称资源规格操作系统安装组件master192.168.18.130k8s-master-012c4g/60gopenEuler 22.03 LTSkube-apiserver、kube-controller-manager、kube-scheduler、etcd、containerd、kubelet、kube-proxy、keepalived、nginx、calico、metrics-server、dashboardworker node192.168.18.131k8s-node-012c4g/60gopenEuler 22.03 LTScontainerd、kubelet、kube-proxy、ingress-controller、calico,corednsworker node192.168.18.132k8s-node-022c4g/60gopenEuler 22.03 LTScontainerd、kubelet、kube-proxy、ingress-controller、calico,coredns关于 VMware Workstation Pro v16.2.2 虚拟机自行下载,并安装配置好,vm 系统使用华为 openEuler 22.03 LTS ISO 镜像。注意:VMware 中网络配置选择【NAT模式】,确保 vm 内部网络环境可以访问到外部网络环境。3. k8s 组件版本关于 k8s 的常用资源组件版本信息规划如下:名称版本下载地址kubernetesv1.24.1https://github.com/kubernetes/kubernetes/releases/tag/v1.24.1kubelet、kubeadm、kubectlv1.24.1yum install -y kubelet-1.24.1 kubeadm-1.24.1 kubectl-1.24.1containerdv1.6.4,cni v0.3.1https://github.com/containerd/containerd/releases/tag/v1.6.4flannelv0.18.0https://github.com/flannel-io/flannel/releases/tag/v0.18.0calicov3.23.1https://github.com/projectcalico/calico/releases/tag/v3.23.1kube-state-metricsv2.4.2https://github.com/kubernetes/kube-state-metrics/releases/tag/v2.4.2metrics-server-helm-chartv3.8.2https://github.com/kubernetes-sigs/metrics-server/releases/tag/metrics-server-helm-chart-3.8.2Kong Ingress Controller for Kubernetes (KIC)v2.3.1https://github.com/Kong/kubernetes-ingress-controller/releases/tag/v2.3.1dashboardv2.5.1https://github.com/kubernetes/dashboard/releases/tag/v2.5.14. 关于 openEuler基于华为 openEuler 系统环境部署,推荐使用 openEuler 22.03 LTS 和 openEuler 20.03 LTS SP3,以 root 身份执行下面命令。为了方便操作,vm 中的 openEuler 系统网络 ip 可以按照上面的编排规划,设置静态 ip 地址。关于 openEuler 系统的安装,请自行参考官方文档,此处不是重点,接下来介绍 openEuler 系统安装后,我们需要设置的相关事项。openEuler 资源地址:ISO下载,https://www.openeuler.org/zh/download/安装指南,https://docs.openeuler.org/zh/docs/22.03_LTS/docs/Installation/installation.html5. shell 终端以下是一些比较常用的 shell 终端,选择自己喜欢的一个安装配置即可。Xshell 5/6/7Windows PowerShell / PowerShellWindows TerminalPuTTYVisual Studio CodeVM 系统部署操作事项(所有节点)注意:下面命令在 k8s 所有节点(master + worker)执行。1. 关闭防火墙 Firewalld防火墙 firewalld 先 stop 再 disable,操作如下:systemctl stop firewalld #停止 $ systemctl disable firewalld #开机禁用 $查看防火墙状态systemctl status firewalld #查看状态输出如下信息,说明已经关闭[root@k8s-master-01 ~]# systemctl status firewalld ● firewalld.service - firewalld - dynamic firewall daemon Loaded: loaded (/usr/lib/systemd/system/firewalld.service; disabled; vendor preset: enabled) Active: inactive (dead) Docs: man:firewalld(1)[Linux防火墙操作命令,开放或关闭端口] =》 https://zhuanlan.zhihu.com/p/1611967112. 关闭 SELinux安全增强型 Linux(SELinux)是一个 Linux 内核的功能,它提供支持访问控制的安全政策保护机制。# 临时关闭 SELinux。 setenforce 0 # 永久关闭 SELinux。 vi /etc/selinux/config SELINUX=disabled验证 selinux 状态getenforce输出如下信息,说明已经关闭[root@k8s-node-01 ~]# getenforce DisabledSELinux 状态说明:SELinux 状态为 disabled,表明 SELinux 已关闭;SELinux 状态为 enforcing 或者 permissive,表明 SELinux 在运行;3. 关闭 Swapswap 的用途 ?swap 分区就是交换分区,(windows 平台叫虚拟内存) 在物理内存不够用时,操作系统会从物理内存中把部分暂时不被使用的数据转移到交换分区,从而为当前运行的程序留出足够的物理内存空间。为什么要关闭 swap ?swap 启用后,在使用磁盘空间和内存交换数据时,性能表现会较差,会减慢程序执行的速度。有的软件的设计师不想使用交换分区,例如:kubelet 在 v1.8 版本以后强制要求 swap 必须关闭,否则会报错:Running with swap on is not supported, please disable swap! or set --fail-swap-on flag to false或者 kubeadm init 时会报错:[ERROR Swap]: running with swap on is not supported. Please disable swap关闭 swapswapoff -a # 临时关闭 vi /etc/fstab # 永久关闭,注释掉 swap 这行查看 swap 是否关闭[root@k8s-master-01 ~]# free -m total used free shared buff/cache available Mem: 1454 881 147 73 425 179 Swap: 0 0 0显示 total/used/free 为 0,说明已经关闭。4. 设置宿主机名称依据宿主机(vm 或物理机)资源编排情况,使用 systemd 里面的 hostnamectl 设置主机名。# 临时 hostnamectl set-hostname k8s-master-01 hostnamectl set-hostname k8s-node-01 hostnamectl set-hostname k8s-node-02 # 永久,编写对应的 hostname vi /etc/hostname设置完成后,重新进入下 shell 终端,使配置生效。bash5. 在 Master node 和 Worker node 添加 hosts修改 hosts 文件,配置主机名称和 ip 之间的映射。$ cat > /etc/hosts << EOF 192.168.18.130 k8s-master-01 192.168.18.131 k8s-node-01 192.168.18.132 k8s-node-02 EOF进入 bash 访问测试 node 节点网络是否连通bash ping k8s-master-01 ping k8s-node-01 ping k8s-node-026. 创建 containerd.conf 配置文件在路径 "/etc/modules-load.d/containerd.conf" 创建配置文件。cat << EOF > /etc/modules-load.d/containerd.conf overlay br_netfilter EOF执行以下命令使配置生效modprobe overlay modprobe br_netfilter7. 将桥接的 IPv4 流量传递到 IPTABLES 链IPTABLES 规则实现 Docker 或 K8s 的网络通信,非常关键。查看 "/etc/sysctl.d/99-xxx.conf" 配置文件。[root@k8s-master-01 /]# ls /etc/sysctl.d/ 99-kubernetes-cri.conf 99-sysctl.conf [root@k8s-master-01 /]# cat /etc/sysctl.d/99-sysctl.conf # sysctl settings are defined through files in # /usr/lib/sysctl.d/, /run/sysctl.d/, and /etc/sysctl.d/. # Vendors settings live in /usr/lib/sysctl.d/. # To override a whole file, create a new file with the same in # /etc/sysctl.d/ and put new settings there. To override # only specific settings, add a file with a lexically later # name in /etc/sysctl.d/ and put new settings there. # For more information, see sysctl.conf(5) and sysctl.d(5). kernel.sysrq=0 net.bridge.bridge-nf-call-ip6tables = 1 net.bridge.bridge-nf-call-iptables = 1 user.max_user_namespaces=28633 net.ipv4.ip_forward=1 net.ipv4.conf.all.send_redirects=0 net.ipv4.conf.default.send_redirects=0 net.ipv4.conf.all.accept_source_route=0 net.ipv4.conf.default.accept_source_route=0 net.ipv4.conf.all.accept_redirects=0 net.ipv4.conf.default.accept_redirects=0 net.ipv4.conf.all.secure_redirects=0 net.ipv4.conf.default.secure_redirects=0 net.ipv4.icmp_echo_ignore_broadcasts=1 net.ipv4.icmp_ignore_bogus_error_responses=1 net.ipv4.conf.all.rp_filter=1 net.ipv4.conf.default.rp_filter=1 net.ipv4.tcp_syncookies=1 kernel.dmesg_restrict=1 net.ipv6.conf.all.accept_redirects=0 net.ipv6.conf.default.accept_redirects=0修改内核参数(上面的 99-sysctl.conf 配置文件已经修)# 1、加载br_netfilter模块 modprobe br_netfilter # 2、验证模块是否加载成功 lsmod | grep br_netfilter # 3、修改内核参数 cat > /etc/sysctl.d/99-sysctl.conf <<EOF net.bridge.bridge-nf-call-ip6tables = 1 net.bridge.bridge-nf-call-iptables = 1 user.max_user_namespaces=28633 net.ipv4.ip_forward = 1 # 4、使刚才修改的内核参数生效,此处使用的是 99-sysctl.conf 配置文件。 sysctl -p /etc/sysctl.d/99-sysctl.confsysctl 命令用于运行时配置内核参数,这些参数位于 "/proc/sys" 目录下。sysctl 配置与显示在 "/proc/sys" 目录中的内核参数。可以用 sysctl 来设置或重新设置联网功能,如 IP 转发、IP 碎片去除以及源路由检查等。用户只需要编辑 "/etc/sysctl.conf" 文件,即可手工或自动执行由 sysctl 控制的功能。8. 配置服务器支持开启 IPVS 的前提条件(K8s 推荐配置)IPVS 称之为 IP 虚拟服务器(IP Virtual Server,简写为 IPVS)。是运行在 LVS 下的提供负载均衡功能的一种技术。IPVS 基本上是一种高效的 Layer-4 交换机,它提供负载平衡的功能。由于 IPVS 已经加入到了 Linux 内核的主干,所以为 kube-proxy(Kubernetes Service) 开启 IPVS 的前提需要加载以下的 Linux 内核模块:ip_vs ip_vs_rr ip_vs_wrr ip_vs_sh nf_conntrack_ipv4 # 或 nf_conntrack在所有服务器集群节点上执行以下脚本cat > /etc/sysconfig/modules/ipvs.modules <<EOF #!/bin/bash modprobe -- ip_vs modprobe -- ip_vs_rr modprobe -- ip_vs_wrr modprobe -- ip_vs_sh modprobe -- nf_conntrack_ipv4 # 若提示在内核中找不到 nf_conntrack_ipv4, 可以让是切换 nf_conntrack # 或者 grep -e ip_vs -e nf_conntrack chmod 755 /etc/sysconfig/modules/ipvs.modules && bash /etc/sysconfig/modules/ipvs.modules && lsmod | grep -e ip_vs -e nf_conntrack_ipv4上面脚本创建了的 "/etc/sysconfig/modules/ipvs.modules" 文件,保证在节点重启后能自动加载所需模块。 可执行命令 【lsmod | grep -e ip_vs -e nf_conntrack_ipv4】查看所需内核模块是否正确加载。接下来还需要确保各个节点上已经安装了 ipset 软件包,为了便于查看 ipvs 的代理规则,最好安装一下管理工具 ipvsadm。yum install -y ipset ipvsadm如果不满足以上前提条件,则即使 kube-proxy 的配置开启了 ipvs 模式,也会退回到 iptables 模式。9. 同步系统时间ntpdate 指令通过轮询指定为服务器参数的 网络时间协议(NTP) 服务器来设置本地日期和时间,从而确定正确的时间。此命令的适用范围:RedHat、RHEL、Ubuntu、CentOS、Fedora。# 安装 ntpdate yum install ntpdate -y # 执行同步命令 ntpdate time.windows.com # 跟网络源做同步 ntpdate cn.pool.ntp.org # 把时间同步做成计划任务 crontab -e * */1 * * * /usr/sbin/ntpdate cn.pool.ntp.org # 重启crond服务 service crond restart # 查看当前时区 date -R同步系统时间输出如下信息[root@k8s-master-01 /]# ntpdate time.windows.com 29 May 16:19:17 ntpdate[7873]: adjust time server 20.189.79.72 offset +0.001081 secIPVS 和 IPTABLES 对比分析IPVS 是什么?IPVS (IP Virtual Server) 实现了传输层负载均衡,也就是我们常说的 4 层局域网交换机(LAN Switches),作为 Linux 内核的一部分。IPVS 运行在主机上,在真实服务器集群前充当负载均衡器。IPVS 可以将基于 TCP 和 UDP 的服务请求转发到真实服务器上,并使真实服务器的服务在单个 IP 地址上显示为虚拟服务。IPVS 和 IPTABLES 对比分析kube-proxy 支持 iptables 和 ipvs 两种模式, 在 kubernetes v1.8 中引入了 ipvs 模式,在 v1.9 中处于 beta 阶段,在 v1.11 中已经正式可用了。iptables 模式在 v1.1 中就添加支持了,从 v1.2 版本开始 iptables 就是 kube-proxy 默认的操作模式,ipvs 和 iptables 都是基于 netfilter 的,但是 ipvs 采用的是 hash 表,因此当 service 数量达到一定规模时,hash 查表的速度优势就会显现出来,从而提高 service 的服务性能。那么 ipvs 模式和 iptables 模式之间有哪些差异呢?ipvs 为大型集群提供了更好的可扩展性和性能ipvs 支持比 iptables 更复杂的复制均衡算法(最小负载、最少连接、加权等等)ipvs 支持服务器健康检查和连接重试等功能在 k8s 的集群环境中,推荐配置 ipvs ,为一定数量规模的 service 提高服务性能。安装 containerd/kubeadm/kubelet(所有节点)Containerd 简介Containerd 是一个工业级标准的容器运行时,它强调简单性、健壮性和可移植性,具备如下功能:管理容器的生命周期(从创建容器到销毁容器)拉取/推送容器镜像存储管理(管理镜像及容器数据的存储)调用 runc 运行容器(与 runc 等容器运行时交互)管理容器网络接口及网络自 Kubernetes v1.24 起,Dockershim 已被删除,由于社区的关注,Docker 和 Mirantis 共同决定继续以 [cri-dockerd] 的形式支持 dockershim 代码(https://www.mirantis.com/blog/the-future-of-dockershim-is -cri-dockerd/), 允许你在需要时继续使用 Docker Engine 作为容器运行时。对于想要尝试其他运行时(如 containerd 或 cri-o) 的用户,已编写迁移文档(https://kubernetes.io/zh/docs/tasks/administer-cluster/migrating-from-dockershim/change-runtime-containerd/)。查看删除 Dockershim 的原因Dockershim:历史背景 =》 https://kubernetes.io/zh/blog/2022/05/03/dockershim-historical-context/如果试图将链从最终用户(user)绘制到实际的容器进程(runc),它可能如下所示:runc 是一个命令行客户端,用于运行根据 OCI(开放容器计划,Open Container Initiative/OCI) 格式打包的应用程序,并且是 OCI 规范的兼容实现。(推荐)使用 Containerd 的理由Kubernetes 在 v1.23 版本及以后版本不再默认采用 Docker/Dockershim ,而建议采用 Containerd;Containerd 比 Docker/Dockershim 更加轻量级,在生产环境中使用更佳合适(稳定,性能);Containerd 安装下面我们进行 containerd 容器运行时的安装,操作如下:1. 安装 wget (可选)由于 openEuler 系统本身集成了 curl ,安装 wget 不是必须项。# 安装 wget sudo yum install wget -y # 此处安装的 wget 版本信息 Installed: wget-1.20.3-2.oe1.x86_642. 下载 containerd分别使用 wget 和 curl 下载 containerd v1.6.4 版本。# 使用 wget 下载 containerd v1.6.4 sudo wget https://github.com/containerd/containerd/releases/download/v1.6.4/cri-containerd-cni-1.6.4-linux-amd64.tar.gz # 使用 curl 下载 containerd v1.6.4 curl -L https://github.com/containerd/containerd/releases/download/v1.6.4/cri-containerd-cni-1.6.4-linux-amd64.tar.gz -O cri-containerd-cni-1.6.4-linux-amd64.tar.gz3. 安装 tartar 命令简介Linux tar(英文全拼:tape archive )命令用于备份文件。tar 是用来建立,还原备份文件的工具程序,它可以加入,解开备份文件内的文件。# 安装 tar sudo yum install -y tar # 查看压缩包包含哪些文件 sudo tar -tf cri-containerd-cni-1.6.4-linux-amd64.tar.gz # 将压缩包解压至 cri-containerd-cni 文件夹(这里用命令创建 cri-containerd-cni文件夹,因为没有实现创建该文件夹),防止将文件都解压缩到当前文件夹(可以看到有etc、opt、usr三个子文件夹)。 sudo tar xzf cri-containerd-cni-1.6.4-linux-amd64.tar.gz -C cri-containerd-cni | mkdir cri-containerd-cnicri-containerd-cni-1.6.4-linux-amd64.tar.gz 文件中包含三个文件夹,分别是【etc、opt、usr】,如下图所示:4. 安装 containerd# 解压 containerd 到根目录 tar zxvf cri-containerd-cni-1.6.0-linux-amd64.tar.gz -C / # 生成 containerd 默认配置 mkdir -p /etc/containerd containerd config default > /etc/containerd/config.toml注意:确认 containerd 可执行文件所在目录在 PATH 环境变量中。5. 配置 containerd 软件源参考配置文件 "/etc/containerd/config.toml" 如下:disabled_plugins = [] imports = [] oom_score = 0 plugin_dir = "" required_plugins = [] root = "/var/lib/containerd" state = "/run/containerd" version = 2 [cgroup] path = "" [debug] address = "" format = "" gid = 0 level = "" uid = 0 [grpc] address = "/run/containerd/containerd.sock" gid = 0 max_recv_message_size = 16777216 max_send_message_size = 16777216 tcp_address = "" tcp_tls_cert = "" tcp_tls_key = "" uid = 0 [metrics] address = "" grpc_histogram = false [plugins] [plugins."io.containerd.gc.v1.scheduler"] deletion_threshold = 0 mutation_threshold = 100 pause_threshold = 0.02 schedule_delay = "0s" startup_delay = "100ms" [plugins."io.containerd.grpc.v1.cri"] disable_apparmor = false disable_cgroup = false disable_hugetlb_controller = true disable_proc_mount = false disable_tcp_service = true enable_selinux = false enable_tls_streaming = false ignore_image_defined_volumes = false max_concurrent_downloads = 3 max_container_log_line_size = 16384 netns_mounts_under_state_dir = false restrict_oom_score_adj = false #sandbox_image = "k8s.gcr.io/pause:3.6" # 1. 修改基础镜像地址(此处以阿里云为例) #sandbox_image = "registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.6" sandbox_image = "registry.aliyuncs.com/google_containers/pause:3.7" selinux_category_range = 1024 stats_collect_period = 10 stream_idle_timeout = "4h0m0s" stream_server_address = "127.0.0.1" stream_server_port = "0" systemd_cgroup = false tolerate_missing_hugetlb_controller = true unset_seccomp_profile = "" [plugins."io.containerd.grpc.v1.cri".cni] bin_dir = "/opt/cni/bin" conf_dir = "/etc/cni/net.d" conf_template = "" max_conf_num = 1 [plugins."io.containerd.grpc.v1.cri".containerd] default_runtime_name = "runc" disable_snapshot_annotations = true discard_unpacked_layers = false no_pivot = false snapshotter = "overlayfs" [plugins."io.containerd.grpc.v1.cri".containerd.default_runtime] base_runtime_spec = "" container_annotations = [] pod_annotations = [] privileged_without_host_devices = false runtime_engine = "" runtime_root = "" runtime_type = "" [plugins."io.containerd.grpc.v1.cri".containerd.default_runtime.options] [plugins."io.containerd.grpc.v1.cri".containerd.runtimes] [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] base_runtime_spec = "" container_annotations = [] pod_annotations = [] privileged_without_host_devices = false runtime_engine = "" runtime_root = "" runtime_type = "io.containerd.runc.v2" [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] BinaryName = "" CriuImagePath = "" CriuPath = "" CriuWorkPath = "" IoGid = 0 IoUid = 0 NoNewKeyring = false NoPivotRoot = false Root = "" ShimCgroup = "" SystemdCgroup = false [plugins."io.containerd.grpc.v1.cri".containerd.untrusted_workload_runtime] base_runtime_spec = "" container_annotations = [] pod_annotations = [] privileged_without_host_devices = false runtime_engine = "" runtime_root = "" runtime_type = "" [plugins."io.containerd.grpc.v1.cri".containerd.untrusted_workload_runtime.options] [plugins."io.containerd.grpc.v1.cri".image_decryption] key_model = "node" [plugins."io.containerd.grpc.v1.cri".registry] config_path = "" [plugins."io.containerd.grpc.v1.cri".registry.auths] [plugins."io.containerd.grpc.v1.cri".registry.configs] [plugins."io.containerd.grpc.v1.cri".registry.headers] [plugins."io.containerd.grpc.v1.cri".registry.mirrors] # 2. 设置仓库地址(镜像源) [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"] endpoint = ["https://usydjf4t.mirror.aliyuncs.com","https://mirror.ccs.tencentyun.com","https://registry.docker-cn.com","http://hub-mirror.c.163.com"] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."k8s.gcr.io"] endpoint = ["https://registry.cn-hangzhou.aliyuncs.com/google_containers"] # [plugins."io.containerd.grpc.v1.cri".registry.mirrors."192.168.0.187:5000"] # endpoint = ["http://192.168.0.187:5000"] # [plugins."io.containerd.grpc.v1.cri".registry.configs] # [plugins."io.containerd.grpc.v1.cri".registry.configs."192.168.0.187:5000".tls] # insecure_skip_verify = true # [plugins."io.containerd.grpc.v1.cri".registry.configs."harbor.creditgogogo.com".auth] # username = "admin" # password = "Harbor12345" [plugins."io.containerd.grpc.v1.cri".x509_key_pair_streaming] tls_cert_file = "" tls_key_file = "" [plugins."io.containerd.internal.v1.opt"] path = "/opt/containerd" [plugins."io.containerd.internal.v1.restart"] interval = "10s" [plugins."io.containerd.metadata.v1.bolt"] content_sharing_policy = "shared" [plugins."io.containerd.monitor.v1.cgroups"] no_prometheus = false [plugins."io.containerd.runtime.v1.linux"] no_shim = false runtime = "runc" runtime_root = "" shim = "containerd-shim" shim_debug = false [plugins."io.containerd.runtime.v2.task"] platforms = ["linux/amd64"] [plugins."io.containerd.service.v1.diff-service"] default = ["walking"] [plugins."io.containerd.snapshotter.v1.aufs"] root_path = "" [plugins."io.containerd.snapshotter.v1.btrfs"] root_path = "" [plugins."io.containerd.snapshotter.v1.devmapper"] async_remove = false base_image_size = "" pool_name = "" root_path = "" [plugins."io.containerd.snapshotter.v1.native"] root_path = "" [plugins."io.containerd.snapshotter.v1.overlayfs"] root_path = "" [plugins."io.containerd.snapshotter.v1.zfs"] root_path = "" [proxy_plugins] [stream_processors] [stream_processors."io.containerd.ocicrypt.decoder.v1.tar"] accepts = ["application/vnd.oci.image.layer.v1.tar+encrypted"] args = ["--decryption-keys-path", "/etc/containerd/ocicrypt/keys"] env = ["OCICRYPT_KEYPROVIDER_CONFIG=/etc/containerd/ocicrypt/ocicrypt_keyprovider.conf"] path = "ctd-decoder" returns = "application/vnd.oci.image.layer.v1.tar" [stream_processors."io.containerd.ocicrypt.decoder.v1.tar.gzip"] accepts = ["application/vnd.oci.image.layer.v1.tar+gzip+encrypted"] args = ["--decryption-keys-path", "/etc/containerd/ocicrypt/keys"] env = ["OCICRYPT_KEYPROVIDER_CONFIG=/etc/containerd/ocicrypt/ocicrypt_keyprovider.conf"] path = "ctd-decoder" returns = "application/vnd.oci.image.layer.v1.tar+gzip" [timeouts] "io.containerd.timeout.shim.cleanup" = "5s" "io.containerd.timeout.shim.load" = "5s" "io.containerd.timeout.shim.shutdown" = "3s" "io.containerd.timeout.task.state" = "2s" [ttrpc] address = "" gid = 0 uid = 0被修改的 "/etc/containerd/config.toml" 文件配置说明:1、基础镜像设置sandbox_image 设置国内基础镜像源地址。2、镜像仓库设置"docker.io" 配置 docker 公共镜像仓库源。"k8s.gcr.io" 配置 k8s 仓库源。其中 “192.168.0.187:5000” 是私人仓库地址(没有可以配置)。insecure_skip_verify = true 意为跳过 tls 证书认证。"harbor.creditgogogo.com".auth 设置仓库用户名和密码。6. 启动 containerd 并设置为开机启动由于上面下载的 containerd 压缩包中包含一个 "etc/systemd/system/containerd.service" 的文件,这样我们就可以通过 systemd 来配置 containerd 作为守护进程运行。[root@k8s-master-01 /]# ls etc/systemd/system/ bluetooth.target.wants cron.service dbus-org.bluez.service default.target multi-user.target.wants sockets.target.wants timedatex.service containerd.service ctrl-alt-del.target dbus-org.freedesktop.nm-dispatcher.service getty.target.wants network-online.target.wants sysinit.target.wants timers.target.wants启动 containerd:# 启动 containerd,并设置为开机启动 systemctl daemon-reload && systemctl enable containerd && systemctl start containerd # 查看 containerd 状态 systemctl status containerd # 重启 containerd systemctl restart containerd此处启动 containerd 可能会显示如下信息:System has not been booted with systemd as init system (PID 1). Can't operate. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. [Unit] Description=containerd container runtime Documentation=https://containerd.io After=network.target local-fs.target [Service] ExecStartPre=-/sbin/modprobe overlay ExecStart=/usr/local/bin/containerd Type=notify Delegate=yes KillMode=process Restart=always RestartSec=5 # Having non-zero Limit*s causes performance problems due to accounting overhead # in the kernel. We recommend using cgroups to do container-local accounting. LimitNPROC=infinity LimitCORE=infinity LimitNOFILE=infinity # Comment TasksMax if your systemd version does not supports it. # Only systemd 226 and above support this version. TasksMax=infinity OOMScoreAdjust=-999 [Install] WantedBy=multi-user.target这里有两个重要的参数:Delegate:这个选项允许 containerd 以及运行时自己管理自己创建容器的 cgroups。如果不设置这个选项,systemd 就会将进程移到自己的 cgroups 中,从而导致 containerd 无法正确获取容器的资源使用情况。KillMode:这个选项用来处理 containerd 进程被杀死的方式。默认情况下,systemd 会在进程的 cgroup 中查找并杀死 containerd 的所有子进程。KillMode 字段可以设置的值如下:control-group:当前控制组里面的所有子进程,都会被杀掉process:只杀主进程mixed:主进程将收到 SIGTERM 信号,子进程收到 SIGKILL 信号none:没有进程会被杀掉,只是执行服务的 stop 命令注意:需要将 KillMode 的值设置为 process,这样可以确保升级或重启 containerd 时不杀死现有的容器。7. 查看 containerd 信息crictl info # 更多使用 crictl --helpK8s 配置阿里云 repo 文件(yum 软件源)默认配置是国外的镜像源,由于国内网络原因无法访问,所以配置阿里云的 yum 软件源。cat <<EOF > /etc/yum.repos.d/kubernetes.repo [kubernetes] name=Kubernetes baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/ enabled=1 gpgcheck=1 repo_gpgcheck=1 gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg EOF阿里云 Kubernetes 镜像:【容器】 https://developer.aliyun.com/mirror/?spm=a2c6h.13651102.0.0.3e221b11RAcHlc&serviceType=mirror&tag=%E5%AE%B9%E5%99%A8【Kubernetes 镜像源】 https://developer.aliyun.com/mirror/kubernetes安装 kubeadm、kubelet 和 kubectl(所有节点)由于版本更新频繁,因此这里指定版本号部署。yum install -y kubelet-1.24.1 kubeadm-1.24.1 kubectl-1.24.1启动 kubelet 并设置开机启动systemctl enable kubelet && systemctl start kubeletps: 由于官网未开放同步方式,可能会有索引 gpg 检查失败的情况,这时请用【yum install -y --nogpgcheck kubelet kubeadm kubectl 】安装。部署 K8s Master 节点1. K8s 集群初始化(kubeadm init)依据上面的 vm 资源规划,在 master 节点(192.168.18.130)执行如下命令:kubeadm init \ --apiserver-advertise-address=192.168.18.130 \ --image-repository registry.aliyuncs.com/google_containers \ --kubernetes-version v1.24.1 \ --service-cidr=10.96.0.0/12 \ --pod-network-cidr=10.244.0.0/16 \ --ignore-preflight-errors=all参数说明:--apiserver-advertise-address 集群通告地址--image-repository 由于默认拉取镜像地址 k8s.gcr.io 国内无法访问,这里指定阿里云镜像仓库地址--kubernetes-version K8s 版本,与上面安装的一致--service-cidr 集群内部虚拟网络,Pod 统一访问入口--pod-network-cidr Pod网络,与下面部署的 CNI 网络组件 yaml 中保持一致--ignore-preflight-errors 忽略所有预检项的警告信息或者使用配置文件引导:$ vi kubeadm.conf apiVersion: kubeadm.k8s.io/v1beta2 kind: ClusterConfiguration kubernetesVersion: v1.24.1 imageRepository: registry.aliyuncs.com/google_containers networking: podSubnet: 10.244.0.0/16 serviceSubnet: 10.96.0.0/12 $ kubeadm init --config kubeadm.conf --ignore-preflight-errors=all kubeadm init 初始化成功,输出如下信息:Your Kubernetes control-plane has initialized successfully! To start using your cluster, you need to run the following as a regular user: mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config Alternatively, if you are the root user, you can run: export KUBECONFIG=/etc/kubernetes/admin.conf You should now deploy a pod network to the cluster. Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at: https://kubernetes.io/docs/concepts/cluster-administration/addons/ Then you can join any number of worker nodes by running the following on each as root: kubeadm join 192.168.18.130:6443 --token pc5d3x.9ccv3m5y1llljk90 \ --discovery-token-ca-cert-hash sha256:3f7b37c18a5ec21c3e225025e13a0ac53e7abdf717859808b24f9bc909b32b5b此处注意保存下 kubeadm init 初始化产生的信息,方便下面环节的部署操作使用。2. 拷贝文件到默认路径拷贝 kubectl 使用的连接 k8s 认证文件到默认路径mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config3. 查看 k8s 集群节点信息[root@k8s-master-01 ~]# kubectl get node NAME STATUS ROLES AGE VERSION k8s-master-01 Ready control-plane 4h8m v1.24.1 k8s-node-01 Ready <none> 3h14m v1.24.1 k8s-node-02 Ready <none> 3h14m v1.24.14. 查看 containerd 拉取的镜像[root@k8s-master-01 /]# crictl image ls IMAGE TAG IMAGE ID SIZE docker.io/calico/cni v3.23.1 90d97aa939bbf 111MB docker.io/calico/node v3.23.1 fbfd04bbb7f47 76.6MB registry.aliyuncs.com/google_containers/coredns v1.8.6 a4ca41631cc7a 13.6MB registry.aliyuncs.com/google_containers/etcd 3.5.3-0 aebe758cef4cd 102MB registry.aliyuncs.com/google_containers/kube-apiserver v1.24.1 e9f4b425f9192 33.8MB registry.aliyuncs.com/google_containers/kube-controller-manager v1.24.1 b4ea7e648530d 31MB registry.aliyuncs.com/google_containers/kube-proxy v1.24.1 beb86f5d8e6cd 39.5MB registry.aliyuncs.com/google_containers/kube-scheduler v1.24.1 18688a72645c5 15.5MB registry.aliyuncs.com/google_containers/pause 3.7 221177c6082a8 311kB注意:上面的镜像除了 calico 相关的(docker.io/calico/cni 和 docker.io/calico/node) 之外,其他都是 kubectl int 初始化拉取的镜像资源。Worker 节点加入 K8s 集群1. k8s 集群环境加入新的 worker node向集群添加新 node 节点,执行在 kubeadm init 输出的 kubeadm join 命令,分别在 worker node 执行如下命令:kubeadm join 192.168.18.130:6443 --token pc5d3x.9ccv3m5y1llljk90 \ --discovery-token-ca-cert-hash sha256:3f7b37c18a5ec21c3e225025e13a0ac53e7abdf717859808b24f9bc909b32b5b2. 生成加入 k8s 集群环境的 token默认 token 有效期为 24 小时,当过期之后,该 token 就不可用了。这时就需要重新创建 token,操作如下:$ kubeadm token create $ kubeadm token list $ openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -hex | sed 's/^.* //' 63bca849e0e01691ae14eab449570284f0c3ddeea590f8da988c07fe2729e924 $ kubeadm join 192.168.31.61:6443 --token nuja6n.o3jrhsffiqs9swnu --discovery-token-ca-cert-hash sha256:63bca849e0e01691ae14eab449570284f0c3ddeea590f8da988c07fe2729e924或者直接命令快捷生成kubeadm token create --print-join-command参考文档 =》https://kubernetes.io/docs/reference/setup-tools/kubeadm/kubeadm-join/部署容器网络/CNI(所有节点)1. CNI 简介CNI 全称是 Container Network Interface,即容器网络的 API 接口。它是 K8s 中标准的一个调用网络实现的接口。Kubelet 通过这个标准的 API 来调用不同的网络插件以实现不同的网络配置方式。CNI 插件就是实现了一系列的 CNI API 接口。常见的 CNI 插件包括 Calico、Flannel、Terway、Weave Net 以及 Contiv。2. CNI 分类及选型参考CNI 插件可以分为三种:Overlay、路由及 Underlay。【Overlay 模式】的典型特征是容器独立于主机的 IP 段,这个 IP 段进行跨主机网络通信时是通过在主机之间创建隧道的方式,将整个容器网段的包全都封装成底层的物理网络中主机之间的包。该方式的好处在于它不依赖于底层网络;【路由模式】中主机和容器也分属不同的网段,它与 Overlay 模式的主要区别在于它的跨主机通信是通过路由打通,无需在不同主机之间做一个隧道封包。但路由打通就需要部分依赖于底层网络,比如说要求底层网络有二层可达的一个能力;【Underlay 模式】中容器和宿主机位于同一层网络,两者拥有相同的地位。容器之间网络的打通主要依靠于底层网络。因此该模式是强依赖于底层能力的。对于 CNI 插件的选择,有以下几个维度参考:环境限制,不同环境中所支持的底层能力是不同的。功能需求性能需求了解更多可以参考 =》https://www.kubernetes.org.cn/6908.html注意:CNI 插件只需要部署其中一个即可,这里推荐 Calico 或 Flannel。3. Calico 简介Calico 是一个纯三层的数据中心网络方案,Calico 支持广泛的平台,包括 Kubernetes、OpenStack 等。Calico 在每一个计算节点利用 Linux Kernel 实现了一个高效的虚拟路由器( vRouter) 来负责数据转发,而每个 vRouter 通过 BGP 协议负责把自己上运行的 workload 的路由信息向整个 Calico 网络内传播。此外,Calico 项目还实现了 Kubernetes 网络策略,提供 ACL 功能。4. 下载 Calico 插件wget https://docs.projectcalico.org/manifests/calico.yaml5. 修改里面定义的 Pod 网络下载完后还需要修改里面定义 Pod 网络(CALICO_IPV4POOL_CIDR),与前面 kubeadm init 指定的一样【--pod-network-cidr=10.244.0.0/16】# The default IPv4 pool to create on startup if none exists. Pod IPs will be # chosen from this range. Changing this value after installation will have # no effect. This should fall within `--cluster-cidr`. # - name: CALICO_IPV4POOL_CIDR # value: "192.168.0.0/16" - name: CALICO_IPV4POOL_CIDR value: "10.244.0.0/16"查找 CALICO_IPV4POOL_CIDRvi calico.yaml /CALICO_IPV4POOL_CIDR6. 应用 calico.yaml 配置修改完后应用 calico.yaml 清单kubectl apply -f calico.yaml输出如下信息:[root@k8s-master-01 /]# ls bin boot calico.yaml cri-containerd-cni-1.6.4-linux-amd64.tar.gz dev etc home lib lib64 lost+found media mnt opt proc root run sbin srv sys tmp usr var [root@k8s-master-01 /]# kubectl apply -f calico.yaml configmap/calico-config created customresourcedefinition.apiextensions.k8s.io/bgpconfigurations.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/bgppeers.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/blockaffinities.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/caliconodestatuses.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/clusterinformations.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/felixconfigurations.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/globalnetworkpolicies.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/globalnetworksets.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/hostendpoints.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/ipamblocks.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/ipamconfigs.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/ipamhandles.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/ippools.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/ipreservations.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/kubecontrollersconfigurations.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/networkpolicies.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/networksets.crd.projectcalico.org created clusterrole.rbac.authorization.k8s.io/calico-kube-controllers created clusterrolebinding.rbac.authorization.k8s.io/calico-kube-controllers created clusterrole.rbac.authorization.k8s.io/calico-node created clusterrolebinding.rbac.authorization.k8s.io/calico-node created daemonset.apps/calico-node created serviceaccount/calico-node created poddisruptionbudget.policy/calico-kube-controllers created7. 查看命名空间 kube-system 下的 podkubectl get pods -n kube-system输出如下信息:[root@k8s-master-01 /]# kubectl get pods -n kube-system NAME READY STATUS RESTARTS AGE calico-kube-controllers-56cdb7c587-mfl9d 1/1 Running 22 (21m ago) 126m calico-node-2dtr8 0/1 Running 35 (5s ago) 126m calico-node-hmdkf 0/1 CrashLoopBackOff 35 (2m6s ago) 126m calico-node-l6448 0/1 Error 38 (70s ago) 126m coredns-74586cf9b6-b5r68 1/1 Running 0 7h5m coredns-74586cf9b6-jndhp 1/1 Running 0 7h6m etcd-k8s-master-01 1/1 Running 0 7h9m kube-apiserver-k8s-master-01 1/1 Running 6 (20m ago) 7h9m kube-controller-manager-k8s-master-01 0/1 CrashLoopBackOff 23 (119s ago) 7h9m kube-proxy-ctx7q 1/1 Running 0 7h6m kube-proxy-l72sj 1/1 Running 0 6h15m kube-proxy-s65x9 1/1 Running 0 6h15m kube-scheduler-k8s-master-01 0/1 CrashLoopBackOff 21 (119s ago) 7h9m测试 K8s 集群验证 Pod 工作验证 Pod 网络通信验证 DNS 解析在 K8s 集群中创建一个 Pod,验证是否正常运行:kubectl create deployment nginx --image=nginx:latest -p 80:8080 kubectl expose deployment nginx --port=8080 --type=NodePort kubectl get pod,svc访问 nginx 的 svc 地址:http://NodeIP:Port 关于 K8s nodePort、port、targetPort、hostPort 端口的介绍 =》https://www.jianshu.com/p/8275f2031c83在 Master 节点部署 Dashboard1. 下载 dashboard 的 recommended.yaml 配置文件wget https://raw.githubusercontent.com/kubernetes/dashboard/v2.5.1/aio/deploy/recommended.yaml2. recommended.yaml 文件重命名(可选)把下载的 recommended.yaml 文件重命名为 dashboard-v2.5.1.yaml (可选,方便文件记录)mv recommended.yaml dashboard-v2.5.1.yaml 3. 修改 Dashboard 配置文件暴露到外部访问默认 Dashboard 只能集群内部访问,修改 Service 为 NodePort 类型,暴露到外部访问:$ vi dashboard-v2.5.1.yaml kind: Service apiVersion: v1 metadata: labels: k8s-app: kubernetes-dashboard name: kubernetes-dashboard namespace: kubernetes-dashboard spec: ports: - port: 443 targetPort: 8443 nodePort: 30001 selector: k8s-app: kubernetes-dashboard type: NodePort ---4. 执行 dashboard-v2.5.1.yaml 配置文件kubectl apply -f dashboard-v2.5.1.yaml 输出如下信息:[root@k8s-master-01 /]# kubectl apply -f /root/dashboard-v2.5.1.yaml namespace/kubernetes-dashboard created serviceaccount/kubernetes-dashboard created service/kubernetes-dashboard created secret/kubernetes-dashboard-certs created secret/kubernetes-dashboard-csrf created secret/kubernetes-dashboard-key-holder created configmap/kubernetes-dashboard-settings created role.rbac.authorization.k8s.io/kubernetes-dashboard created clusterrole.rbac.authorization.k8s.io/kubernetes-dashboard created rolebinding.rbac.authorization.k8s.io/kubernetes-dashboard created clusterrolebinding.rbac.authorization.k8s.io/kubernetes-dashboard created deployment.apps/kubernetes-dashboard created service/dashboard-metrics-scraper created deployment.apps/dashboard-metrics-scraper created5. 查看 kubernetes-dashboardkubectl get pods -n kubernetes-dashboard输出如下信息:[root@k8s-master-01 /]# kubectl get pods -n kubernetes-dashboard NAME READY STATUS RESTARTS AGE dashboard-metrics-scraper-7bfdf779ff-q9tsk 0/1 ContainerCreating 0 74s kubernetes-dashboard-6465b7f54c-frf2b 0/1 ContainerCreating 0 74s [root@k8s-master-01 /]# kubectl get pods -n kubernetes-dashboard NAME READY STATUS RESTARTS AGE dashboard-metrics-scraper-7bfdf779ff-q9tsk 1/1 Running 0 32m kubernetes-dashboard-6465b7f54c-frf2b 1/1 Running 0 32m6. 访问 kubernetes-dashboard访问 kubernetes-dashboard 地址https://NodeIP:30001创建 ServiceAccount 并绑定默认 cluster-admin 管理员集群角色:# 创建用户 $ kubectl create serviceaccount dashboard-admin -n kube-system # 用户授权 $ kubectl create clusterrolebinding dashboard-admin --clusterrole=cluster-admin --serviceaccount=kube-system:dashboard-admin # 获取用户 Token $ kubectl describe secrets -n kube-system $(kubectl -n kube-system get secret | awk '/dashboard-admin/{print $1}')此处 master 节点的 ip 是 192.168.18.130,浏览器访问地址 =》https://192.168.18.130:30001/#/login最后使用输出的 token 登录 k8s-Dashboard。总结搭建 k8s 集群环境时,事先要规划编排好相关的资源环境以及各个组件的版本信息,开始部署之前务必保障系统环境准备事项的相关设置操作,此处我们采用的容器运行时是 containerd,由于默认的 yum 镜像源是国外环境,在国内网络环境无法访问,可配置国内的 yum 源(比如:阿里云 yum 镜像源)保障网络资源的连通性和可访问性,方便后面环节的相关操作(比如:containerd/kubeadm/kubelet 的安装、k8s master 节点的部署、容器网络 CNI 安装及网络编排)。每一个环节都很重要,中途遇到异常,多查看资料分析下原因,关键在于理解。
01 linux常用命令和vim的使用
1 管理文件目录结构与管理1.1 linux命令格式:命令条件和参数操作对象(目录or文件)1.2 常用命令命令说明备注创建目录mkdir test在当前目录下创建test目录mkdir 是Make directories缩写删除命令rm test.java 删除 test.java 文件rm 是remove的缩写查看目录内容ls查看当前目录里的内容ls 是 list缩写查看文件详细信息ls -l权限分别对应三组权限:用户、同组用户、其他用户查看用户当前所在目录pwd打印出当前用户所在目录pwd 是print work dir缩写更改目录cd /从当前目录进入根目录cd 是Change Directory 的缩写查看当前系统用户whoami查看当前系统用户返回上次工作目录cd -修改文件名mv /zinksl/test.java /zinksl/mv.java修改test.java为mv.java mv 是move 的缩写查看文件cat mv.java查看mv.java文件中的内容cat是concatenate的缩写更改用户权限chmod 777 mv.javamv.java文件所有用户都可读可写、可执行chmod是Change mode的缩写清空屏幕clear管理应用程序systemctl start nginx启动Nginxx程序systemctl stop nginx结束Nginx程序检查程序的进程ps -ef 列出机器上所有进程信息ps 是Process status 的缩写ps -ef | grep “nginx” 查询 Nginx的进程信息查询端口信息netstat -tunlplinux过滤命令grep 搜索过滤某些多余信息验证网站信息curl -I 162.14.109.137查看主机地址为162.14.109.137的网站服务器信息查看软件安装信息rpm -ql nginx查看Nginx的安装信息 1.3 常见的目录含义目录名作用备注dev存放抽象硬件lib存放系统库文件sbin存放特权二进制文件var存放经常变化的文件日志等home普通用户目录etc存放配置文件目录boot存放内核与启动文件bin存放二进制文件(可执行命令)usr存放安装程序(软件默认目录)root 特权用户目录opt大型软件存放目录mnt文件挂载目录(U盘、光驱等)2 文件权限管理权限是针对用户而言的,root用户拥有最高权限更改文件权限chmod 777 mv.javamv.java文件所有用户都可读可写、可执行chmod是Change mode的缩写3 用户管理命令说明备注创建用户useradd user1创建user1用户改密码 passwd user1 666 user1密码改为666删除用户 userdel user1删除用户保留家目录userdel -r user1用户和家目录都删除(要慎重)查询用户信息 id user1用户ID,用户组who am i 登录时间,登录ip…3.1 用户组:命令作用命令说明备注创建用户组groupadd group1创建用户组 :group1删除用户组groupdel group1删除用户组:group1创建用户并指定组名useradd -g group1 user1创建用户user1并加入group1修改用户的组usermod -g 用户组 用户名3.2 切换用户:从超级当前用户切换到其他用户:su 用户名退出登录logout3.3 用户相关信息 文件相关文件 用途说明/etc/passwd用户配置文件,记录用户各种信息每行就是一个用户,用户名:口令:用户标识号:组标识号:注释性描述:主(家)目录:登录shell/etc/shadow口令的配置文件/etc/group 组的配置文件,记录Linux包含组的信息每行含义,组名:口令:组标识号:组内用户列表4 vi或vim的使用这个编辑器有三种模式:4.1 正常模式 :(1)进入正常模式使用,vi 或 vim +文件名 进入正常模式(2)在正常模式下进行复制、粘贴、删除相关操作模式命令 说明备注正常模式yy复制6yy复制当前行以下6行p粘贴dd删除6dd删除当前行以下6行/hello查找hello 回车 输入n切换查找:set nonu设置不显示行号:set nu设置显示行号G跳到尾行gg跳到首行u撤销动作20 Shift+g 定位到第20行命令行模式编辑模式4.2 编辑模式:进入正常模式后,输入 i、a、o、r的大小写都可以进入编辑模式4.3 命令行模式:在普通模式下按esc键 + : wq 即可退出到命令行模式下5 运行级别修改运行级别:常用3,5init 运行级别运行级别【代号】说明备注0备注1单用户【找回丢失的密码】2多用户状态无网络服务3多用户转态有网络服务multi-user.target4系统未使用保留给用户5图形界面graphical.target6系统重启查看当前运行级别systemctl get-defautl设置系统默认运行级别systemctl set-default 运行级别6 目录相关操作命令说明备注mkdir testDocument创建单级目录testDocumentmkdir -p testD1/d2创建多级目录testD1和d2rmdir d2删除空文件夹 d2
深入理解rtmp(二)之C++脚手架搭建
前面深入理解rtmp(1)之开发环境搭建中我们已经搭建好服务器,并且利用一些现成的工具可以推送直播流,播放直播流了.这篇文章我们开始搭建从零开发一套rtmp推流拉流sdk,对着协议实现,达到真正的"深入理解".作为一个码农,搬砖搬到一定高度就需要"脚手架"来支撑我们"够得住".为了方面我们把rtmp推拉流sdk实现为一个PC上的命令行程序,当开发调试稳定后,我们可以快速的通过交叉编译工具编译到Android/iOS等移动端设备.1.创建工程我们使用cmake作为安装编译工具,需要安装cmake,mac下执行brew install cmake. 在我们的rtmpsdk路径下创建CMakeLists.txt://指定cmake最低版本 cmake_minimum_required (VERSION 3.6) set(CMAKE_INSTALL_PREFIX "${CMAKE_BINARY_DIR}" CACHE PATH "Installation directory" FORCE) message(STATUS "CMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX}") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -fPIC -ffunction-sections -fdata-sections -Os") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fPIC -ffunction-sections -fdata-sections -Os") project (rtmpsdk) set(SRC_PREFIX "src") set(SELF_LIBS_OUT ${CMAKE_SYSTEM_NAME}.out) file(GLOB SELF_SRC_FILES ${SRC_PREFIX}/main.cpp add_executable(${PROJECT_NAME} ${SELF_SRC_FILES})创建src目录,创建main.cpp文件:#include <iostream> int main(int argc,char* argv[]) //标准输出到控制台 std::cout << "Hello rtmp server!" << std::endl; return 0; }在rtmpsdk下创建cmake_build文件夹作为我们的输出路径 在控制台,我们进入我们的工程路径后执行:cd cmake_build然后执行:cmake .. make 在camke下面生成了编译中间文件和最终的rtmpsdk文件:现在执行一下./rtmpsdk:$ ./rtmpsdk Hello rtmp server!可以看到我们打印的"Hello rtmp server!",编译环境已经搭建好了,可以继续往下实现我们的功能了.注:我的开发环境是mac,windows环境后面我提供一个docker的centos镜像作为我们工程的编译环境.2.封装接口我们想象一下,我们的rtmp应该对外提供什么接口?封装什么数据结构?我们要连接我们的服务器,rtmp是基于tcp,那么我们要创建一个socket网络套接字,那么我们需要一个根据url创建对象的接口rtmp_t rtmp_create(const char* url)创建socket后我们还需要做一些配置,最基本的我们要配置读写超时时间,如果我们的socket没有超时,我们的读写函数一直没有返回,会导致无法退出的问题,所以我们需要提供一个设置读写超时的接口:int rtmp_set_timeout(rtmp_t rtmp, int recv_timeout_ms, int send_timeout_ms)rtmp有握手过程,接下来需要一个握手接口:int rtmp_handshake(rtmp_t rtmp)握手成功后开始连接服务器,提供连接接口:int rtmp_connect_app(rtmp_t rtmp)连接成功后通知服务器是拉流还是推流,提供两个函数:int rtmp_play_stream(rtmp_t rtmp),int rtmp_publish_stream(rtmp_t rtmp)可以开始拉流或推流了:int rtmp_read_packet(rtmp_t rtmp, char* type, uint32_t* timestamp, char** data, int* size),int rtmp_write_packet(rtmp_t rtmp, char type, uint32_t timestamp, char* data, int size)拉推流结束后,销毁对象释放资源:void rtmp_destroy(rtmp_t rtmp)以播放为例用一个图表示:接口定义好了,我们在src下新建libs目录,创建我们对外暴露的rtmpsdk.hpp文件:#ifndef LIB_RTMP_HPP #define LIB_RTMP_HPP * rtmpsdk is a librtmp like library, * used to play/publish rtmp stream from/to rtmp server. * socket: use sync and block socket to connect/recv/send data with server. * depends: no need other libraries; depends on ssl if use complex_handshake. * thread-safe: no #ifndef __STDC_FORMAT_MACROS #define __STDC_FORMAT_MACROS #endif #include <stdint.h> #include <sys/types.h> #ifdef __cplusplus extern "C"{ #endif /************************************************************* ************************************************************** * RTMP protocol context ************************************************************** *************************************************************/ // the RTMP handler. typedef void* rtmp_t; * Create a RTMP handler. * @param url The RTMP url, for example, rtmp://localhost/live/livestream * @remark default timeout to 30s if not set by rtmp_set_timeout. * @remark default schema to url_schema_normal, use rtmp_set_schema to change it. * @return a rtmp handler, or NULL if error occured. extern rtmp_t rtmp_create(const char* url); * set socket timeout * @param recv_timeout_ms the timeout for receiving messages in ms. * @param send_timeout_ms the timeout for sending message in ms. * @remark user can set timeout once rtmp_create, * or before rtmp_handshake or rtmp_dns_resolve to connect to server. * @remark default timeout to 30s if not set by rtmp_set_timeout. * @return 0, success; otherswise, failed. extern int rtmp_set_timeout(rtmp_t rtmp, int recv_timeout_ms, int send_timeout_ms); * close and destroy the rtmp stack. * @remark, user should never use the rtmp again. extern void rtmp_destroy(rtmp_t rtmp); /************************************************************* ************************************************************** * RTMP protocol stack ************************************************************** *************************************************************/ * connect and handshake with server * category: publish/play * previous: rtmp-create * next: connect-app * @return 0, success; otherswise, failed. * simple handshake specifies in rtmp 1.0, * not depends on ssl. * rtmp_handshake equals to invoke: * rtmp_dns_resolve() * rtmp_connect_server() * rtmp_do_simple_handshake() * user can use these functions if needed. extern int rtmp_handshake(rtmp_t rtmp); * Connect to RTMP tcUrl(Vhost/App), similar to flash AS3 NetConnection.connect(tcUrl). * @remark When connected to server, user can retrieve informations from RTMP handler, * for example, use rtmp_get_server_id to get server ip/pid/cid. * @return 0, success; otherswise, failed. extern int rtmp_connect_app(rtmp_t rtmp); * play a live/vod stream. * category: play * previous: connect-app * next: destroy * @return 0, success; otherwise, failed. extern int rtmp_play_stream(rtmp_t rtmp); * publish a live stream. * category: publish * previous: connect-app * next: destroy * @return 0, success; otherwise, failed. extern int rtmp_publish_stream(rtmp_t rtmp); * E.4.1 FLV Tag, page 75 // 8 = audio #define RTMP_TYPE_AUDIO 8 // 9 = video #define RTMP_TYPE_VIDEO 9 // 18 = script data #define RTMP_TYPE_SCRIPT 18 * read a audio/video/script-data packet from rtmp stream. * @param type, output the packet type, macros: * RTMP_TYPE_AUDIO, FlvTagAudio * RTMP_TYPE_VIDEO, FlvTagVideo * RTMP_TYPE_SCRIPT, FlvTagScript * otherswise, invalid type. * @param timestamp, in ms, overflow in 50days * @param data, the packet data, according to type: * FlvTagAudio, @see "E.4.2.1 AUDIODATA" * FlvTagVideo, @see "E.4.3.1 VIDEODATA" * FlvTagScript, @see "E.4.4.1 SCRIPTDATA" * User can free the packet by rtmp_free_packet. * @param size, size of packet. * @return the error code. 0 for success; otherwise, error. * @remark: for read, user must free the data. * @remark: for write, user should never free the data, even if error. * @return 0, success; otherswise, failed. extern int rtmp_read_packet(rtmp_t rtmp, char* type, uint32_t* timestamp, char** data, int* size); // @param data User should never free it anymore. extern int rtmp_write_packet(rtmp_t rtmp, char type, uint32_t timestamp, char* data, int size); #ifdef __cplusplus #endif #endif接口定义好后,我们开始按步骤实现接口,下面我们开始实现第一步rtmp_create,通过url创建socket.3.封装网络接口封装网络接口前,我们先对linux c网络编程做一个回顾3.1linux c socket编程基本流程我们先来一张图:我们的rtmpsdk作为 tcp客户端,我们再一起了解一下linux c关于socket的api3.1.1 socket()函数原型int socket(int domain, int type, int protocol);参数说明domain: 协议域,又称协议族(family)。常用的协议族有 AF_INET 、 AF_INET6 、 AF_LOCAL(或称AF_UNIX,Unix域Socket)、AF_ROUTE 等。协议族决定了 socket 的地址类型,在通信中必须采用对应的地址,如 AF_INET 决定了要用 ipv4 地址(32位的)与端口号(16位的)的组合、AF_UNIX 决定了要用一个绝对路径名作为地址。type: 指定 Socket 类型。常用的 socket 类型有 SOCK_STREAM 、 SOCK_DGRAM 、 SOCK_RAW 、 SOCK_PACKET 、 SOCK_SEQPACKET 等。流式 Socket(SOCK_STREAM)是一种面向连接的 Socket,针对于面向连接的 TCP 服务应用。数据报式 Socket(SOCK_DGRAM)是一种无连接的 Socket,对应于无连接的 UDP 服务应用。protocol: 指定协议。常用协议有 IPPROTO_TCP 、 IPPROTO_UDP 、 IPPROTO_STCP 、 IPPROTO_TIPC 等,分别对应 TCP 传输协议、UDP 传输协议、STCP 传输协议、TIPC 传输协议。注意:1.type 和 protocol 不可以随意组合,如 SOCK_STREAM 不可以跟 IPPROTO_UDP 组合。当第三个参数为0时,会自动选择第二个参数类型对应的默认协议。返回值如果调用成功就返回新创建的套接字的描述符,如果失败就返回 INVALID_SOCKET(Linux下失败返回-1)。套接字描述符是一个整数类型的值。每个进程的进程空间里都有一个套接字描述符表,该表中存放着套接字描述符和套接字数据结构的对应关系。该表中有一个字段存放新创建的套接字的描述符,另一个字段存放套接字数据结构的地址,因此根据套接字描述符就可以找到其对应的套接字数据结构。每个进程在自己的进程空间里都有一个套接字描述符表但是套接字数据结构都是在操作系统的内核缓冲里。3.1.2 bind()bind()函数把一个地址族中的特定地址赋给socket。例如对应 AF_INET、AF_INET6 就是把一个 ipv4 或 ipv6 地址和端口号组合赋给socket。函数原型int bind(int socketfd, const struct sockaddr *addr, socklen_t addrlen);参数说明socketfd: 一个标识已连接套接口的描述字。address: 是一个sockaddr结构指针,该结构中包含了要结合的地址和端口号。address_len: 确定 address 缓冲区的长度。其中,sockaddr 这个地址结构根据地址创建 socket 时的地址协议族的不同而不同。如ipv4对应的是:struct sockaddr_in { sa_family_t sin_family; /* address family: AF_INET */ in_port_t sin_port; /* port in network byte order */ struct in_addr sin_addr; /* internet address */ /* Internet address. */ struct in_addr { uint32_t s_addr; /* address in network byte order */ };ipv6对应的是:struct sockaddr_in6 { sa_family_t sin6_family; /* AF_INET6 */ in_port_t sin6_port; /* port number */ uint32_t sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr; /* IPv6 address */ uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */ struct in6_addr { unsigned char s6_addr[16]; /* IPv6 address */ };Unix域对应的是:#define UNIX_PATH_MAX 108 struct sockaddr_un { sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */ };返回值如果函数执行成功,返回值为0,否则为SOCKET_ERROR。3.1.3listen()如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。函数原型int listen(int socketfd, int backlog);参数说明socketfd: 要监听的socket的描述字。backlog: 相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。3.1.4connect()函数原型int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);参数说明socketfd: 客户端socket的描述字。sockaddr: 服务器的socket地址。addrlen: socket地址的长度3.1.5. accept()TCP服务器端依次调用 socket()、bind()、listen() 之后,就会监听指定的 socket 地址了。TCP客户端依次调用 socket()、connect() 之后就向 TCP 服务器发送了一个连接请求。TCP 服务器监听到这个请求之后,就会调用 accept() 函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。函数原型int accept(int socketfd, struct sockaddr *addr, socklen_t *addrlen); //返回连接connect_fd参数说明socketfd: 就是上面解释中的监听套接字,这个套接字用来监听一个端口,当有一个客户与服务器连接时,它使用这个一个端口号,而此时这个端口号正与这个套接字关联。当然客户不知道套接字这些细节,它只知道一个地址和一个端口号。sockaddr: 结果参数,它用来接受一个返回值,这返回值指定客户端的地址,当然这个地址是通过某个地址结构来描述的,用户应该知道这一个什么样的地址结构。如果对客户的地址不感兴趣,那么可以把这个值设置为NULL。len: 它也是结果的参数,用来接受上述 addr 的结构的大小的,它指明 addr 结构所占有的字节个数。同样的,它也可以被设置为NULL。如果accept成功返回,则服务器与客户已经正确建立连接了,此时服务器通过accept返回的套接字来完成与客户的通信。accept默认会阻塞进程,直到有一个客户连接建立后返回,它返回的是一个新可用的套接字,这个套接字是连接套接字。监听套接字: 监听套接字正如accept的参数sockfd,它是监听套接字,在调用listen函数之后,是服务器开始调用socket()函数生成的,称为监听socket描述字(监听套接字)连接套接字:一个套接字会从主动连接的套接字变身为一个监听套接字;而accept函数返回的是已连接socket描述字(一个连接套接字),它代表着一个网络已经存在的点点连接。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。连接套接字socketfd_new 并没有占用新的端口与客户端通信,依然使用的是与监听套接字socketfd一样的端口号3.1.6. read()、write()等当服务器与客户端已经建立好连接,可以调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信!网络I/O操作有下面几组:read()/write() recv()/send() readv()/writev() recvmsg()/sendmsg() recvfrom()/sendto()函数原型1int recv(SOCKET socket, char FAR* buf, int len, int flags);参数说明1socket: 一个标识已连接套接口的描述字。buf: 用于接收数据的缓冲区。len: 缓冲区长度。flags: 指定调用方式。取值:MSG_PEEK 查看当前数据,数据将被复制到缓冲区中,但并不从输入队列中删除;MSG_OOB 处理带外数据。 若无错误发生,recv()返回读入的字节数。如果连接已中止,返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。函数原型2ssize_t recvfrom(int sockfd, void buf, int len, unsigned int flags, struct socketaddr* from, socket_t* fromlen);参数说明2sockfd: 标识一个已连接套接口的描述字。buf: 接收数据缓冲区。len: 缓冲区长度。flags: 调用操作方式。是以下一个或者多个标志的组合体,可通过or操作连在一起:MSG_DONTWAIT:操作不会被阻塞;MSG_ERRQUEUE: 指示应该从套接字的错误队列上接收错误值,依据不同的协议,错误值以某种辅佐性消息的方式传递进来,使用者应该提供足够大的缓冲区。导致错误的原封包通过msg_iovec作为一般的数据来传递。导致错误的数据报原目标地址作为msg_name被提供。错误以sock_extended_err结构形态被使用。MSG_PEEK:指示数据接收后,在接收队列中保留原数据,不将其删除,随后的读操作还可以接收相同的数据。MSG_TRUNC:返回封包的实际长度,即使它比所提供的缓冲区更长, 只对packet套接字有效。MSG_WAITALL:要求阻塞操作,直到请求得到完整的满足。然而,如果捕捉到信号,错误或者连接断开发生,或者下次被接收的数据类型不同,仍会返回少于请求量的数据。MSG_EOR:指示记录的结束,返回的数据完成一个记录。MSG_TRUNC:指明数据报尾部数据已被丢弃,因为它比所提供的缓冲区需要更多的空间。MSG_CTRUNC:指明由于缓冲区空间不足,一些控制数据已被丢弃。(MSG_TRUNC使用错误,4才是MSG_TRUNC的正确解释)MSG_OOB:指示接收到out-of-band数据(即需要优先处理的数据)。MSG_ERRQUEUE:指示除了来自套接字错误队列的错误外,没有接收到其它数据。from:(可选)指针,指向装有源地址的缓冲区。fromlen:(可选)指针,指向from缓冲区长度值。函数原型3int sendto( SOCKET s, const char FAR* buf, int size, int flags, const struct sockaddr FAR* to, int tolen);参数说明3s: 套接字buf: 待发送数据的缓冲区size: 缓冲区长度flags: 调用方式标志位, 一般为0, 改变Flags,将会改变Sendto发送的形式addr: (可选)指针,指向目的套接字的地址tolen: addr所指地址的长度 如果成功,则返回发送的字节数,失败则返回SOCKET_ERROR。函数原型4int accept( int fd, struct socketaddr* addr, socklen_t* len);参数说明4fd: 套接字描述符。addr: 返回连接着的地址len: 接收返回地址的缓冲区长度 成功返回客户端的文件描述符,失败返回-1。3.1.7. close()在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字。函数原型int close(int fd);close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。3.2封装socket我们把socket和超时配置等封装到一个结构体:struct BlockSyncSocket SOCKET fd; int family; int64_t rbytes; int64_t sbytes; // The send/recv timeout in ms. int64_t rtm; int64_t stm; BlockSyncSocket() { stm = rtm = UTIME_NO_TIMEOUT; rbytes = sbytes = 0; SOCKET_RESET(fd); SOCKET_SETUP(); virtual ~BlockSyncSocket() { SOCKET_CLOSE(fd); SOCKET_CLEANUP(); };通过上面分析知,我们需要设计socket创建,连接,读写,设置超时等:/** * simple socket stream, * use tcp socket, sync block mode class SimpleSocketStream private: BlockSyncSocket* io; public: SimpleSocketStream(); virtual ~SimpleSocketStream(); public: virtual BlockSyncSocket* hijack_io(); virtual int create_socket(std::string url); virtual int connect(const char* server, int port); public: virtual error_t read(void* buf, size_t size, ssize_t* nread); public: virtual void set_recv_timeout(utime_t tm); virtual utime_t get_recv_timeout(); virtual int64_t get_recv_bytes(); public: virtual void set_send_timeout(utime_t tm); virtual utime_t get_send_timeout(); virtual int64_t get_send_bytes(); virtual error_t writev(const iovec *iov, int iov_size, ssize_t* nwrite); public: virtual error_t read_fully(void* buf, size_t size, ssize_t* nread); virtual error_t write(void* buf, size_t size, ssize_t* nwrite); };接下来我们实现网络封装接口:#include <netinet/tcp.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/uio.h> #include <sys/types.h> #include <errno.h> #include <stdio.h> #include <netdb.h> #include <bs_socket.hpp> BlockSyncSocket* hijack_io_create() BlockSyncSocket* skt = new BlockSyncSocket(); return skt; void hijack_io_destroy(BlockSyncSocket* ctx) freep(ctx); int hijack_io_create_socket(BlockSyncSocket* skt,std::string url) skt->family = AF_INET6; skt->fd = ::socket(skt->family, SOCK_STREAM, 0); // Try IPv6 first. if (!SOCKET_VALID(skt->fd)) { skt->family = AF_INET; skt->fd = ::socket(skt->family, SOCK_STREAM, 0); // Try IPv4 instead, if IPv6 fails. if (!SOCKET_VALID(skt->fd)) { return ERROR_SOCKET_CREATE; // No TCP cache. int v = 1; setsockopt(skt->fd, IPPROTO_TCP, TCP_NODELAY, &v, sizeof(v)); return ERROR_SUCCESS; int hijack_io_connect(BlockSyncSocket* skt, const char* server_ip, int port) char sport[8]; snprintf(sport, sizeof(sport), "%d", port); addrinfo hints; memset(&hints, 0, sizeof(hints)); hints.ai_family = skt->family; hints.ai_socktype = SOCK_STREAM; addrinfo* r = NULL; AutoFree(addrinfo, r); if(getaddrinfo(server_ip, sport, (const addrinfo*)&hints, &r)) { return ERROR_SOCKET_CONNECT; if(::connect(skt->fd, r->ai_addr, r->ai_addrlen) < 0){ return ERROR_SOCKET_CONNECT; return ERROR_SUCCESS; int hijack_io_read(BlockSyncSocket* skt, void* buf, size_t size, ssize_t* nread) int ret = ERROR_SUCCESS; ssize_t nb_read = ::recv(skt->fd, (char*)buf, size, 0); if (nread) { *nread = nb_read; // On success a non-negative integer indicating the number of bytes actually read is returned // (a value of 0 means the network connection is closed or end of file is reached). if (nb_read <= 0) { if (nb_read < 0 && SOCKET_ERRNO() == SOCKET_ETIME) { return ERROR_SOCKET_TIMEOUT; if (nb_read == 0) { errno = SOCKET_ECONNRESET; return ERROR_SOCKET_READ; skt->rbytes += nb_read; return ret; int hijack_io_set_recv_timeout(BlockSyncSocket* skt, int64_t tm) // The default for this option is zero, // which indicates that a receive operation shall not time out. int32_t sec = 0; int32_t usec = 0; if (tm != UTIME_NO_TIMEOUT) { sec = (int32_t)(tm / 1000); usec = (int32_t)((tm % 1000)*1000); struct timeval tv = { sec , usec }; if (setsockopt(skt->fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) == -1) { return SOCKET_ERRNO(); skt->rtm = tm; return ERROR_SUCCESS; int hijack_io_set_send_timeout(BlockSyncSocket* skt, int64_t tm) // The default for this option is zero, // which indicates that a receive operation shall not time out. int32_t sec = 0; int32_t usec = 0; if (tm != UTIME_NO_TIMEOUT) { sec = (int32_t)(tm / 1000); usec = (int32_t)((tm % 1000)*1000); struct timeval tv = { sec , usec }; if (setsockopt(skt->fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) == -1) { return SOCKET_ERRNO(); skt->stm = tm; return ERROR_SUCCESS; int hijack_io_writev(BlockSyncSocket* skt, const iovec *iov, int iov_size, ssize_t* nwrite) int ret = ERROR_SUCCESS; ssize_t nb_write = ::writev(skt->fd, iov, iov_size); if (nwrite) { *nwrite = nb_write; // On success, the readv() function returns the number of bytes read; // the writev() function returns the number of bytes written. On error, -1 is // returned, and errno is set appropriately. if (nb_write <= 0) { if (nb_write < 0 && SOCKET_ERRNO() == SOCKET_ETIME) { return ERROR_SOCKET_TIMEOUT; return ERROR_SOCKET_WRITE; skt->sbytes += nb_write; return ret; int hijack_io_read_fully(BlockSyncSocket* skt, void* buf, size_t size, ssize_t* nread) int ret = ERROR_SUCCESS; size_t left = size; ssize_t nb_read = 0; while (left > 0) { char* this_buf = (char*)buf + nb_read; ssize_t this_nread; if ((ret = hijack_io_read(skt, this_buf, left, &this_nread)) != ERROR_SUCCESS) { return ret; nb_read += this_nread; left -= (size_t)this_nread; if (nread) { *nread = nb_read; skt->rbytes += nb_read; return ret; int hijack_io_write(BlockSyncSocket* skt, void* buf, size_t size, ssize_t* nwrite) int ret = ERROR_SUCCESS; ssize_t nb_write = ::send(skt->fd, (char*)buf, size, 0); if (nwrite) { *nwrite = nb_write; if (nb_write <= 0) { if (nb_write < 0 && SOCKET_ERRNO() == SOCKET_ETIME) { return ERROR_SOCKET_TIMEOUT; return ERROR_SOCKET_WRITE; skt->sbytes += nb_write; return ret; error_t SimpleSocketStream::read(void* buf, size_t size, ssize_t* nread) assert(io); int ret = hijack_io_read(io, buf, size, nread); if (ret != ERROR_SUCCESS) { return error_new(ret, "read"); return success; }接下来我们就可以在我们的main函数里面创建SimpleSocketStream,然后创建socket了.下一篇我们开始通过创建的socket进行rtmp握手.3.3测试在我们的main.cpp中:#include <iostream> #include <bs_socket.hpp> int main(int argc,char* argv[]) std::cout << "Hello rtmp server!" << std::endl; SimpleSocketStream *sss = new SimpleSocketStream(); if(sss->create_socket("rtmp://127.0.0.1:1935/live/livestream") != 0){ printf("create socket error!"); return -1; std::cout<< "create fd = " << sss->hijack_io()->fd << std::endl; free(sss); return 0; }输出结果:$ ./rtmpsdk Hello rtmp server! create fd = 3我们成功创建了句柄为3的socket.题外话linux网络编程中有同步/异步,阻塞/非阻塞,由于我们现在sdk是客户端,没有并发连接的问题,所以我们的实现使用阻塞同步socket. 我们在创建socket时兼容了ipv6,先尝试ipv6,如果失败了再尝试ipv4:int hijack_io_create_socket(BlockSyncSocket* skt,std::string url) skt->family = AF_INET6; skt->fd = ::socket(skt->family, SOCK_STREAM, 0); // Try IPv6 first. if (!SOCKET_VALID(skt->fd)) { skt->family = AF_INET; skt->fd = ::socket(skt->family, SOCK_STREAM, 0); // Try IPv4 instead, if IPv6 fails. if (!SOCKET_VALID(skt->fd)) { return ERROR_SOCKET_CREATE; // No TCP cache. int v = 1; setsockopt(skt->fd, IPPROTO_TCP, TCP_NODELAY, &v, sizeof(v)); return ERROR_SUCCESS; }setsockopt可以对socket进行设置,这里:IPPROTO_TCP 和 IPPROTO_IP代表两种不同的协议,分别代表IP协议族里面的TCP协议和IP协议 TCP_NODELAY是什么呢? TCP/IP协议中针对TCP默认开启了Nagle算法。Nagle算法通过减少需要传输的数据包,来优化网络。在内核实现中,数据包的发送和接受会先做缓存,分别对应于写缓存和读缓存。启动TCP_NODELAY,就意味着禁用了Nagle算法,允许小包的发送。对于延时敏感型,同时数据传输量比较小的应用,开启TCP_NODELAY选项无疑是一个正确的选择。rtmp是直播流式传输,对延时比较敏感,所以我们关闭了NODELAY.同时比如,对于SSH会话,用户在远程敲击键盘发出指令的速度相对于网络带宽能力来说,绝对不是在一个量级上的,所以数据传输非常少;而又要求用户的输入能够及时获得返回,有较低的延时。如果开启了Nagle算法,就很可能出现频繁的延时,导致用户体验极差。当然,你也可以选择在应用层进行buffer,比如使用java中的buffered stream,尽可能地将大包写入到内核的写缓存进行发送;vectored I/O(writev接口)也是个不错的选择。对于关闭TCP_NODELAY,则是应用了Nagle算法。数据只有在写缓存中累积到一定量之后,才会被发送出去,这样明显提高了网络利用率(实际传输数据payload与协议头的比例大大提高)。但是这又不可避免地增加了延时;与TCP delayed ack这个特性结合,这个问题会更加显著,延时基本在40ms左右。当然这个问题只有在连续进行两次写操作的时候,才会暴露出来。连续进行多次对小数据包的写操作,然后进行读操作,本身就不是一个好的网络编程模式;在应用层就应该进行优化。 对于既要求低延时,又有大量小数据传输,还同时想提高网络利用率的应用,大概只能用UDP自己在应用层来实现可靠性保证了。
docker 从 0 到 1
概述容器技术对进程进行封装隔离,属于操作系统层面的虚拟化技术。由于隔离的进程独立于宿主和其它的隔离的进程,因此称为容器。Docker 在容器的基础上,进行了进一步的封装,从文件系统、网络互联到进程隔离等等,极大的简化了容器的创建和维护。使得 Docker 技术比虚拟机技术更为轻便、快捷。主要目标:通过对应用组件的封装、分发、部署、运行等生命周期的管理,达到应用级别的一次封装,到处运行。现在基本上所有的开发人员都需要与 docker 打交道,交付方式大多也都是交付 docker 镜像,现代化的开发流程大致如下可以说,现在还不会 docker 的开发,不是一个好开发基本概念组成Docker 的世界里主要由三部分组成:镜像:Image容器:Container仓库:Repository这三个概念贯穿了 Docker 的生命周期,理解了这三个概念,就更容器理清 Docker 的生命周期镜像Docker 镜像是 一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。Docker 设计时,就充分利用 Union FS 的技术,将其设计为分层存储的架构。 镜像实际是由多层文件系统联合组成。镜像构建时,会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。容器镜像和容器的关系,就像是面向对象程序设计中的 类 和 实例 一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以通过 Docker API 创建、启动、停止、删除、暂停等。在默认情况下,容器与其它容器及其主机是隔离的,拥有自己的独立进程空间、网络配置。容器由其镜像以及在创建或启动容器时提供的任何配置选项定义。当容器被删除时,对其状态的任何未存储在持久存储中的更改都会消失。按照 Docker 最佳实践的要求,容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化。所有的文件写入操作,都应该使用 数据卷(Volume)、或者绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。容器 = 镜像 + 读写层容器用来运行程序,是读写层镜像用来安装程序,是制度层仓库仓库是集中存放镜像文件的场所。有时候狐白仓库和仓库注册服务器(Registry)混为一谈,并不严格区分。实际上,仓库注册服务器上往往存放着多个仓库,每个仓库中又包含了多个镜像,每个镜像有不同的标签(tag)。架构中间部位为我们进行 Docker 操作的宿主机,其运行了一个 Docker daemon 的核心守护程序,负责构建、运行和分发 Docker 容器。左边为 Docker 客户端,其与 Docker 守护进程进行通信,客户端会将 build、pull、run 命令发送到 Docker 守护进程进行执行。右边为 Docler 注册表存储 Docker 镜像,是一个所有 Docker 用户共享 Docker 镜像的服务,Docker daemon 与之进行交互。镜像操作docker 的镜像来源大致有这几种:仓库拉取、本地构建、导入。镜像拉取docker 的仓库拉取命令格式如下$ docker pull [仓库地址[:端口]/]仓库名[tag] 复制代码仓库地址可以省略,默认地址是 docker hub;仓库名分为两部分,用户名+镜像名,用户名可以省略,默认为library,也就是官方镜像;标签默认为 latest,即最新版本,可以根据需要指定哪个版本查看镜像使用docker images 查看全部镜像,docker image ls 效果相同,;使用 docker inspect <image-name|image-id>查看镜像数据;可以使用-f 参数查看镜像的指定属性,例如$ docker inspect -f {{.Metadata}} nginx 复制代码双大括号之内的属性要在信息中存在,注意.不要忽略;使用 docker system df 查看镜像占用的磁盘空间;使用docker image ls -f dangling=true查看所有的虚悬镜像,由于虚悬镜像没有用处,因此可以随意删除docker image prune虚悬镜像:由于新旧镜像重名,旧镜像名称被取消,从而出现仓库名、标签名均为 <none> 的镜像。这类无标签镜像也被称为 虚悬镜像(dangling image)删除镜像使用 docker rmi [选项] <image-id> <image-id>来删除镜像,image-id 可以是长 id、短 id、镜像摘要或者镜像名制作镜像使用 docker build 命令手动制作镜像,docker 会根据指定的路径中的 dockerfile 来制作镜像常用构建命令:docker build -t image-name:version .;.表示当前路径下,也可以手动指定 Dockerfile 文件,docker build -f /path/to/Dockerfile传输镜像制作完镜像之后可以推送到仓库,也可已通过 tar 包来进行传输仓库向仓库推送之前需要将镜像打 tag$ docker tag image-name username/image-name:version # 如果没有登录 docker 需要先登录 $ docker login # 推送镜像 $ docker push username/image-name:version 复制代码手动传输先将镜像保存为 tar 包(或者其他包类型),然后通过 scp 等工具将 tar 包传输到其他设备,然后加载 tar 包PS:save 和 load 操作都是相对于当前目录# 导出镜像 $ docker save image-name > filename $ docker save image-name -o filename # 传输镜像包 $ scp filename user@address[:path] $ scp nginx.tar root@10.10.10.10:/ssd # 然后输入目标终端对应用户的密码即可 # 加载镜像 $ docker load < nginx.tar 复制代码Dockerfile 脚本Dockerfile 分为四部分:基础镜像信息、维护者信息、镜像操作指令和容器启动时执行指令。部分指令基础镜像信息FROM维护者信息MAINTAINER镜像操作指令RUN、COPY、ADD、EXPOSE 等容器启动时执行指令CMD、ENTRYPOINT注意,每个指令独立运行,并导致创建一个新的 Image,因此 RUN cd / tmp 对下一个指令不会有任何影响,如果想要移动目录可以使用&连接命令,RUN cd /tmp & ls环境变量环境变量(使用 ENV 语句声明)也可以在某些指令中用作要由 Dockerfile 解释的变量。还可以处理转义,以将类似变量包含在语句中。环境变量在 Dockefile 中用 $variable_name 或 ${variable_name} 表示。它们被等同对待,并且括号语法通常用于解决不带空格的变量名的问题,例如 ${foo}_bar。${variable_name} 语法还支持以下指定的一些标准 bash 修饰符:${variable:-word} 表示如果设置了 variable,则结果将是该值。如果 variable 未设置,那么 word 将是结果。${variable:+word} 表示如果设置了 variable,那么 word 将是结果,否则结果是空字符串在所有情况下,word 可以是任何字符串,包括额外的环境变量。可以通过在变量之前添加 \ 来转义:\$foo 或 \${foo},分别转换为 $foo 和 ${foo}FROM busybox ENV foo /bar WORKDIR ${foo} # WORKDIR /bar ADD . $foo # ADD . /bar COPY \$foo /quux # COPY $foo /quux 复制代码支持环境变量的指令有:ADD、COPY、ENV、EXPOSE、LABEL、USER、WORKDIR、VOLUME、STOPSIGNALFROM构建镜像的基础源镜像(Base Image)。因此,有效的 Dockerfile 必须具有 FROM 作为其第一条指令。FROM <image> FROM <image>:<tag> FROM <image>@<digest> 复制代码FROM 必须是 Dockerfile 中的第一个非注释指令。FROM 可以在单个 Dockerfile 中多次出现,以创建多个图像。只需记下在每个新的 FROM 命令之前由提交输出的最后一个 Image IDtag 或 digest 是可选的。如果省略其中任何一个,构建器将默认使用 latest。如果构建器与 tag 值不匹配,则构建器将返回错误。MAINTAINER设置生成的 Images 的作者字段WORKDIR为 Dockerfile 中的任何 RUN、CMD、ENTRYPOINT、COPY 和 ADD 指令设置工作目录。可以在一个 Dockerfile 中多次使用,如果提供了相对路径,它将相对于先前 WORKDIR 指令的路径。WORKDIR /foo WORKDIR bar WORKDIR baz RUN pwd # /foo/bar/baz 复制代码WORKDIR 指令可以解析先前使用 ENV 设置的环境变量ENV DIRPATH /path WORKDIR $DIRPATH/$DIRNAME RUN pwd # /path/$DIRNAME 复制代码ENVENV 指令将环境变量 <key> 设置为值 <value>。ENV 指令有两种形式。第一种形式,ENV <key> <value>,将单个变量设置为一个值。第一个空格后面的整个字符串将被视为 <value> - 包括空格和引号等字符。第二种形式,ENV <key>=<value> ...,允许一次设置多个变量。第二种形式在语法中使用等号 =,而第一种形式不使用。与命令行解析类似,引号和反斜杠可用于在值内包含空格。ARGARG 指令定义一个变量,用户可以使用 docker build 命令使用 --build-arg <varname>=<value> 标志,在构建时将其传递给构建器。如果用户指定了一个未在 Dockerfile 中定义的构建参数,构建将输出错误。Dockerfile 作者可以通过指定 ARG 一个或多个变量,通过多次指定 ARG 来定义单个变量。例如,一个有效的 Dockerfile:FROM myapp ARG foo ARG bar 复制代码也可以可选地指定 ARG 指令的默认值:FROm myapp ARG foo=xyz ARG bar=123 复制代码如果 ARG 值具有缺省值,并且如果在构建时没有传递值,则构建器使用缺省值。使用上述示例,但使用不同的 ENV 规范,可以在 ARG 和 ENV 指令之间创建更有用的交互:FROM ubuntu ARG CONT_IMG_VER ENV CONT_IMG_VER ${CONT_IMG_VER:-v1.0.0} RUN echo $CONT_IMG_VER 复制代码ARG 变量不会持久化到构建的 Image 中,而 ENV 变量则会。但是,ARG 变量会以类似的方式影响构建缓存。如果一个 Dockerfile 定义一个 ARG 变量,它的值不同于以前的版本,那么在它的第一次使用时会出现一个 “cache miss”,而不是它的定义。特别地,在 ARG 指令之后的所有 RUN 指令都隐式地使用 ARG 变量(作为环境变量),因此可能导致高速缓存未命中。RUNRUN:后面跟的是在容器中要执行的命令。有两种形式:RUN <command> shell 形式,命令在 Shell 中运行,Linux 上为 /bin/sh/ -c,Windows 上为 cmd /S/CRUN ["executable", "param1", "param2"] exec 形式每一个 RUN 指令都会新建立一层,在其上执行这些命令,当运行多个指令时,会产生一些非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错因此,在很多情况下,我们可以合并指令并运行,例如 RUN apt-get update && apt-get install -y libgdiplus。在命令过多时,一定要注意格式,比如换行、缩进、注释等,会让维护、排障更为容易。除此之外,Union FS 是有最大层数限制的,不能超过 127 层,而且我们应该把每一层中我用文件清除,比如一些没用的依赖,来防止镜像臃肿。COPY拷贝文件至容器的工作目录下,.dockerignore 指定的文件不会拷贝。COPY <src> .. <dest> COPY ["<src>", ..., "<dest>"] 复制代码与 ADD 类似,不过 COPY 的 <src> 不能为 URL。如果源或目标包含空格,请将路径括在方括号和双引号中。ADDADD 指令与 COPY 指令非常类似,但它包含更多功能。除了将文件从主机复制到容器映像,ADD 指令还可以使用 URL 规范从远程位置复制文件EXPOSEEXPOSE <port> [<port>...] 复制代码EXPOSE 指令通知 Docker 容器在运行时侦听指定的网络端口。EXPOSE 不使主机的容器的端口可访问。为此,必须使用 -p 标志发布一系列端口,或者使用 -P 标志发布所有暴露的端口。您可以公开一个端口号,并用另一个端口号在外部发布。ENTRYPOINTENTRYPOINT 允许您配置容器,运行执行的可执行文件。# 使用 exec 执行 ENTRYPOINT ["executable", "param1", "param2"] # 使用 shell 执行 ENTRYPOINT command param1 param2 复制代码每个 Dockerfile 中只能有一个 ENTRYPOINT,当指定多个时,只有最后一个起效。CMDCMD 指令有三种形式:# 使用 exec 执行,推荐方式 # 这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 CMD ["executable", "param1", "param2"] # 在 /bin/sh 中执行,提供给需要交互的应用 # 实际命令会被包装为 sh -c 的参数形式进行执行 CMD command param1 param2 # 提供给 ENTRYPOINT 的默认参数 CMD ["param1", "param2"] 复制代码指定启动容器时执行命令,每个 Dockerfile 只能有一个 CMD 指令。如果指定了多条命令,则只有最后一条会被执行。如果用户启动容器时候指定了运行的命令,则会覆盖掉 CMD 指定的命令。与 ENTRYPOINT 对比共同点:都可以指定 Shell 或 exec 函数调用的方式执行命令当存在多个 CMD 指令或 ENTRYPOINT 指令时,只有最后一个生效差异:CMD 指令指定的容器启动时命令可以被 docker run 指定的命令覆盖,而 ENTRYPOINT 指令指定的命令不能被覆盖,而是将 docker run 指定的参数当做 ENTRYPOINT 指定命令的参数CMD 指令可以为 ENTRYPOINT 指令设置默认参数,而且可以被 docker run 指定的参数覆盖总结:如果 ENTRYPOINT 使用了 shell 模式,CMD 指令会被忽略如果 ENTRYPOINT 使用了 exec 模式,CMD 指定的内容被追加为 ENTRYPOINT 指定命令的参数如果 ENTRYPOINT 使用了 exec 模式,CMD 也应该使用 exec 模式exec 模式是建议的使用模式,因为当运行任务的进程作为容器中的 1 号进程时,我们可以通过 docker 的 stop 命令优雅地结束容器。VOLUMEVOLUME 指令创建具有指定名称的挂载点,并将其标记为从本机主机或其他容器保留外部挂载的卷。该值可以是 JSON 数组 VOLUME ["/var/log"] 或具有多个参数的纯字符串,例如 VOLUME /var/log 或 VOLUME /var/log /var/db。USERUSER 指令设置运行 Image 时使用的用户名或 UID,以及 Dockerfile 中的任何 RUN、CMD 和 ENTRYPOINT 指令。LABELLABEL <key>=<value> <key>=<value> <key>=<value> ... 复制代码LABEL 指令向 Image 添加元数据。LABEL 是键值对。要在 LABEL 值中包含空格,使用引号和反斜杠,就像在命令行解析中一样:Image 可以有多个 Label。要指定多个 Label,Docker 建议在可能的情况下将标签合并到单个 LABEL 指令中。每个 LABEL 指令产生一个新层,如果使用许多标签,可能会导致效率低下的镜像。容器操作容器的几个核心状态,也就是图中色块表示的:Created、Running、Paused、Stopped、Deleted。启动容器$ docker run [args] <image-name>[:tag] 复制代码常见的启动参数:-p:向宿主机暴露端口,格式 宿主机端口:容器端口-P:将容器端口映射为宿主机的随机端口-t:让 Docker 分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上-i:让容器的标准输入保持打开-v:映射数据卷,例如 /home/project:/usr/src,宿主机 /home/project 映射容器 /usr/src-d:将容器放在后台运行--rm:容器推出后清除资源--privileged:最高权限当利用 docker run 来创建容器时,Docker 在后台运行的标准操作包括:检查本地是否存在制定的镜像,不存在就从公有仓库下载利用本地镜像创建并启动一个容器分配一个文件系统,并在只读的镜像层外面挂载一层可读写层从宿主机配置的网桥接口桥接一个虚拟接口到容器中去从地址池配置一个 IP 地址给容器执行用户的指定的用户程序执行完毕后容器被终止重启容器$ docker restart <container-name> 复制代码停止容器$ docker stop <container-name> $ docker container stop <container-name> 复制代码使用 stop 停止的容器可以使用 start 再次启动进入容器$ docker exec -it <container-name> /bin/bash 复制代码原理实际上是启动了容器内的 /bin/bash,此时你就可以通过 bash shell 与容器内交互了。就像远程连接了 SSH 一样。-i:只有该参数时,由于没有分配伪终端,界面没有我们熟悉的 Linux 命令提示符,但命令执行结果仍然可以返回-it:当合并使用时,则可以看到我们熟悉的 Linux 命令提示符如果从这个 stdin 中 exit,不会导致容器的停止。如果启动容器时使用-dit后台启动了终端,可以通过 attach 连接$ docker attach <container-name> 复制代码导入、导出容器# 导出容器 $ docker export <container-name> > <file-name>.<file-suffix> # 导入容器 $ cat <import-file-name> | docker import - <image-name> # 格式 $ cat ubuntu.tar | docker import - test/ubuntu:v1.0 # 示例 # 也可以通过 URL 导入 $ docker import <url> # 格式 $ docker import http://example.com/exampleimage.tgz example/imagerepo # 示例 复制代码删除容器# 删除处于终止状态的容器 $ docker rm <container-name> # 删除运行状态的容器 $ docker rm -f <container-name> # 清理掉所有处于终止状态的容器 $ docker container prune 复制代码网络要实现网络通信,机器需要至少一个网络接口(物理接口或虚拟接口)来收发数据包;此外,如果不同子网之间要进行通信,需要路由机制。Docker 中的网络接口默认都是虚拟的接口。虚拟接口的优势之一是转发效率较高。 Linux 通过在内核中进行数据复制来实现虚拟接口之间的数据转发,发送接口的发送缓存中的数据包被直接复制到接收接口的接收缓存中。对于本地系统和容器内系统看来就像是一个正常的以太网卡,只是它不需要真正同外部网络设备通信,速度要快很多。Docker 容器网络就利用了这项技术。它在本地主机和容器内分别创建一个虚拟接口,并让它们彼此连通(这样的一对接口叫做 veth pair)。Docker 中使用 Network 来管理容器之间的通信,只要两个 Conteiner 处于同一个 Network 之中,就可以通过容器名去互相通信。网络模式在 docker 的世界中有四种网络模式:Bridge 模式Host 模式Container 模式None 模式Bridge 模式Bridge 模式(--net=bridge)为 Docker 容器默认创建后的网络模式,这中模式创建后即加入 docker0 网桥。其特点如下:使用一个 Linux bridge,默认为 docker0使用 veth 对,一头在容器的网络 namespace 中,一头在 docker0 上该模式下 Docker Container 不具有一个公有 IP,因为宿主机的 IP 地址与 veth pair 的 IP 地址不在同一个网段内Docker 采用 NAT 方式,将容器内部的服务监听的端口与宿主机的某一个端口 port 进行绑定,使得宿主机以外的世界可以主动将网络报文发送至容器内部外界访问容器内的服务时,需要访问宿主机的 IP 以及宿主机的端口 portNAT 模式由于是在三层网络上的实现手段,故肯定会影响网络的传输效率容器拥有独立、隔离的网络栈;让容器和宿主机以外的世界通过 NAT 建立通信Host 模式Host 模式(--net=host)并没有为容器创建一个隔离的网络环境(network namespace),这就意味着容器不会有自己的网卡信息,而是与宿主机共用网络环境,亦即拥有完全的本地主机接口的访问权限。容器进程可以跟宿主机其他 root 进程一样打开低范围的端口,可以访问本地网络服务比如 D-Bus,还可以让容器做一些影响整个主机系统的事情。这种情况下,容器除了网络,其他都是隔离的。此时容器内获取 IP 为宿主机 IP,端口绑定直接绑定在宿主机网卡上,有点是网络传输不用经过 NAT 转换,效率更高速度更快。其特点包括:这种模式下的容器没有隔离的 network namespace容器的 IP 地址同 Docker host(容器的宿主机)的 IP 地址需要注意容器中服务的端口号不能与 Docker host 上已经使用的端口号相冲突host 模式能够和其它模式共存Container 模式Container 模式(--net=container:<NAME_OR_ID>)是 Docker 中一种较为特别的网络的模式。处于这个模式下的 Docker 容器会共享其他容器的网络环境(共享 network namespace),因此,至少这两个容器之间不存在网络隔离,而这两个容器又与宿主机以及除此之外其他的容器存在网络隔离。两个容器有自己的文件系统、进程列表和资源限制,但会和已存在的容器共享 IP 地址和端口等网络资源,两者进程可以直接通过 I/O 环回接口通信。None 模式None 模式(--net=none),即容器获取独立的 network namespace,但不为容器进行任何网络配置,需要手动配置。一旦 Docker 容器采用了 none 网络模式,那么容器内部就只能使用 loopback 网络设备,不会再有其他的网络资源。Docker Container 的 none 网络模式意味着不给该容器创建任何网络环境,容器只能使用 127.0.0.1 的本机网络。容器互联# 通过link指令建立连接 $ docker run --name <Name> -d -p <path1>:<path2> --link <containerName>:<alias> <containerName:tag/imageID> 复制代码常用指令$ docker network create # 将容器 container-name 连接到新建网络 network-name $ docker network connect <network-name> <contaienr-name> $ docker network ls # 将 container-name 从 network-name 网络中移除连接 $ docker network disconnect <network-name> <container-name> # 与 disconnect 相似,但是要求容器关闭或断开与此网络的连接 $ docker network rm <network-name> <container-name> # 查看容器的网络情况 $ docker network inspect <container-name>
DevOps工程师学习路径
DevOps方法论DevOps方法论的主要来源是Agile, Lean 和TOC, 独创的方法论是持续交付。DevOps 是一种软件开发方法,涉及持续开发,持续测试,持续集成,部署和监视。这一系列过程跨越了传统上孤立的开发和运营团队,DevOps 试图消除它们之间的障碍。因此,DevOps 工程师基本上与 Development 和 Operations 团队合作,DevOps 是这两个主要部分之间的链接。DevOps经典图书《DevOps实践指南》《持续交付:发布可靠软件的系统方法》&《持续交付 2.0》《凤凰项目》《Effective DevOps》必备技能DevOps 包括诸如构建自动化、CI/CD、基础架构即代码等概念,并且有许多工具可以实现这些概念。由于这些工具数量众多,因此可能会造成混乱和压倒性的结果。最重要的是要了解概念,并为每个类别的学习找一种特定的工具。例如,当你已经知道什么是 CI/CD 并知道如何使用 Jenkins 时,也将很容易学习同类型的其他替代工具。接下来让就来看看学习 DevOps 需要掌握哪些技能。1)软件开发的概念作为一名 DevOps 工程师,你不会直接对应用程序进行编程,但是当你与开发团队紧密合作以改善和自动化他们的任务时,你需要了解以下概念:开发人员的工作方式他们正在使用哪个 git 工作流程如何配置应用程序自动化测试2)操作系统作为 DevOps 工程师,你负责准备在操作系统上部署应用程序的所需要的基础结构环境。并且由于大多数服务器是 Linux 服务器,因此你需要了解 Linux 操作系统,并善于使用命令行,所以你需要知道:基本的 Shell 命令Linux 文件系统管理服务器的基础知识SSH 密钥管理在服务器上安装不同的工具3)网络与安全你还需要了解网络和安全性的基础知识才能配置基础架构,例如:配置防火墙以保护应用程序了解 IP 地址,端口和 DNS 的工作方式负载均衡器代理服务器HTTP/HTTPS但是,要在 DevOps 和 IT Operations 之间划清界线,你不是系统管理员。因此,在这里不需要高级知识,理解和了解基本知识就够了。IT 方面是这些 SysAdmins,Networking 或 Security Engineers 人的专长。4)容器化随着容器成为新标准,你可能会将应用程序作为容器运行,这意味着你需要大致了解:虚拟化的概念容器的概念学习哪个工具?Docker - 当今最受欢迎的容器技术5)持续集成和部署在 DevOps 中,所有代码更改(例如开发人员的新功能和错误修复)都应集成到现有应用程序中,并以自动化方式连续地部署到最终用户。因此,建立完整的 CI/CD 管道是 DevOps 工程师的主要任务和职责。在完成功能或错误修正后,应自动触发在 CI 服务器(例如 Jenkins )上运行的管道,该管道:运行测试打包应用程序构建 Docker 镜像将 Docker Image 推送到工件存储库,最后将新版本部署到服务器(可以是开发,测试或生产服务器)因此,你需要在此处学习技能:设置 CI/CD 服务器构建工具和程序包管理器工具以执行测试并打包应用程序配置工件存储库(例如 Nexus,Artifactory)当然,可以集成更多的步骤,但是此流程代表 CI/CD 管道的核心,并且是 DevOps 任务和职责的核心。学习哪个工具?Jenkins 是最受欢迎的人之一。其他:Bamboo,Gitlab,TeamCity,CircleCI,TravisCI。6)云提供商如今,许多公司正在使用云上的虚拟基础架构,而不是管理自己的基础架构。这些是基础架构即服务(IaaS)平台,可提供一系列服务,例如备份,安全性,负载平衡等。因此,你需要学习云平台的服务。例如。对于 AWS,你应该了解以下基本知识:IAM 服务-管理用户和权限VPC 服务-你的专用网络EC2 服务-虚拟服务器AWS 提供了更多的服务,但是你只需要了解你实际需要的服务即可。例如,当 K8s 集群在 AWS 上运行时,你还需要学习 EKS 服务。AWS 是功能最强大,使用最广泛的一种,但也是最困难的一种。学习哪个工具?AWS 是最受欢迎的一种。其他热门:Azure,Google Cloud,阿里云,腾讯云。7)容器编排如前所述,容器已被广泛使用,在大公司中,成百上千个容器正在多台服务器上运行,这意味着需要以某种方式管理这些容器。为此目的,有一些容器编排工具,而最受欢迎的是 Kubernetes。因此,你需要学习:Kubernetes 如何工作管理和管理 Kubernetes 集群并在其中部署应用程序学习哪个工具?Kubernetes - 最受欢迎。8)监视和日志管理软件投入生产后,对其进行监视以跟踪性能,发现基础结构以及应用程序中的问题非常重要。因此,作为 DevOps 工程师的职责之一是:设置软件监控设置基础架构监控,例如用于你的 Kubernetes 集群和底层服务器。学习哪个工具?Prometheus, Grafana...9)基础设施即代码手动创建和维护基础架构非常耗时且容易出错,尤其是当你需要复制基础架构时,例如用于开发,测试和生产环境。在 DevOps 中,希望尽可能地自动化,那就是将“基础结构即代码(Infrastructure as Configuration)”引入其中。因此使用 IaC ,我们将使用代码来创建和配置基础结构,你需要了解两种 IaC 方式:基础设施配置配置管理使用这些工具,可以轻松地复制和恢复基础结构。因此,你应该在每个类别中都知道一种工具,以使自己的工作更有效率,并改善与同事的协作。学习哪个工具?基础架构设置:Terraform 是最受欢迎的一种。配置管理:Ansible,Puppet,Chef。10)脚本语言作为 DevOps 工程师就常见的工作就是编写脚本和小型的应用程序以自动化任务。为了能够做到这一点,你需要了解一种脚本或编程语言。这可能是特定于操作系统的脚本语言,例如 bash 或 Powershell。还需要掌握一种独立于操作系统的语言,例如 Python 或 Go。这些语言功能更强大,更灵活。如果你善于使用其中之一,它将使你在就业市场上更具价值。学习哪个工具?Python:目前是最需要的一个,它易于学习,易于阅读并且具有许多可用的库。其他:Go,NodeJS,Ruby。11)版本控制上述所有这些自动化逻辑都作为代码编写,使用版本控制工具(例如Git)来管理这些代码和配置文件。学习哪个工具?Git - 最受欢迎和广泛使用。网站和博客DEOS :DevOps 国际峰会,以案例总结著称;DevOpsDays:大名鼎鼎的 DevOpsDays 社区;TheNewStack :综合性网站,盛产高质量的电子书;DevOps.com :综合性网站;DZone : 综合性网站,盛产高质量的电子书;Azure DevOps:综合性网站,盛产高质量的电子书;Martin Fowler :Martin Fowler 的博客;CloudBees Devops :Jenkins 背后的公司的博客。
比 Nginx 性能更强的下一代 Web 服务器
原创 | Java 2021 超神之路,很肝~中文详细注释的开源项目RPC 框架 Dubbo 源码解析网络应用框架 Netty 源码解析消息中间件 RocketMQ 源码解析数据库中间件 Sharding-JDBC 和 MyCAT 源码解析作业调度中间件 Elastic-Job 源码解析分布式事务中间件 TCC-Transaction 源码解析Eureka 和 Hystrix 源码解析Java 并发源码来源:民工哥技术之路简介特性安装配置模块化配置实例总结简介Caddy 2 是一个强大的、企业级的、开源的 Web 服务器。是一个 Go 编写的 Web 服务器,类似于 Nginx,Caddy 提供了更加强大的功能。支持 HTTP/2 的 Web 服务端。它使用 Golang 标准库提供 HTTP 功能。Caddy 一个显著的特性是默认启用HTTPS。它是第一个无需额外配置即可提供HTTPS 特性的Web 服务器。官网:https://caddyserver.com文档:https://caddyserver.com/docs/基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能项目地址:https://github.com/YunaiV/ruoyi-vue-pro视频教程:https://doc.iocoder.cn/video/特性全自动支持 HTTP/2 协议,无需配置。全自动变成 HTTPS 站点,无需任何配置。合理使用多核支持 IPv6 环境对 WebSockets 有很好的支持可以自动把 Markdown 转成 HTMLCaddy 对 log 格式的定义很容易易于部署 ,没有依赖作为反向代理支持主动和被动健康检查、负载均衡、断路、缓存等功能。支持 Windows、 Linux、Mac 三大主流系统相较于 Nginx 来说使用 Caddy 的优势如下自动的 HTTPS 证书申请(ACME HTTP/DNS 挑战)自动证书续期以及 OCSP stapling 等更高的安全性包括但不限于 TLS 配置以及内存安全等友好且强大的配置文件支持支持 API 动态调整配置(有木有人可以搞个 Dashboard)支持 HTTP3(QUIC)支持动态后端,例如连接 Consul、作为 k8s ingress 等后端多种负载策略以及健康检测等本身 Go 编写,高度模块化的系统方便扩展(CoreDNS 基于 Caddy1 开发)基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能项目地址:https://github.com/YunaiV/yudao-cloud视频教程:https://doc.iocoder.cn/video/安装下载地址:https://caddyserver.com/downloadFedora/RHEL/CentOS 8$ dnf install 'dnf-command(copr)' $ dnf copr enable @caddy/caddy $ dnf install caddyMacOSbrew install caddyRHEL/CentOS 7$ yum install yum-plugin-copr $ yum copr enable @caddy/caddy $ yum install caddyDebian/Ubuntu/Raspbian$ sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https $ curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo tee /etc/apt/trusted.gpg.d/caddy-stable.asc $ curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list $ sudo apt update $ sudo apt install caddy安装完成后,在终端命令行下输入 caddy :$ caddy Caddy is an extensible server platform. usage: caddy <command> [<args...>] commands: adapt Adapts a configuration to Caddy's native JSON build-info Prints information about this build environ Prints the environment file-server Spins up a production-ready file server fmt Formats a Caddyfile hash-password Hashes a password and writes base64 help Shows help for a Caddy subcommand list-modules Lists the installed Caddy modules reload Changes the config of the running Caddy instance reverse-proxy A quick and production-ready reverse proxy run Starts the Caddy process and blocks indefinitely start Starts the Caddy process in the background and then returns stop Gracefully stops a started Caddy process trust Installs a CA certificate into local trust stores untrust Untrusts a locally-trusted CA certificate upgrade Upgrade Caddy (EXPERIMENTAL) validate Tests whether a configuration file is valid version Prints the version Use 'caddy help <command>' for more information about a command. Full documentation is available at:常用命令$ systemctl start[stop|restart] caddy //启动服务 $ systemctl reload caddy //重新加载配置文件 $ systemctl status caddy //查看运行状态配置Caddy2 的配置文件核心采用 json,但是 json 可读性不强,所以官方维护了一个转换器,抽象出称之为 Caddyfile 的新配置格式;关于 Caddyfile 的完整语法请查看官方文档。文档地址:https://caddyserver.com/docs/caddyfile常用配置站点配置(site_option) { encode zstd gzip file_server handle_errors { rewrite * /{http.error.status_code}.html file_server import acme_https import log_file root * /www/{host} }多域名配置地址 { ........ }这个地址支持以下几种格式localhost example.com http://example.com localhost:8080 127.0.0.1 [::1]:2015 example.com/foo/* *.example.com http://全局配置选项{ # 开启所有的调试输出 debug # 设定默认的sni default_sni domain.com # 打开或关闭管理端口 admin off }错误页面跳转www.mingongge.com { root * /web/mingongge.com/ file_server handle_errors { rewrite * /{http.error.status_code}.html file_server }Log 日志(log_file) { log { format logfmt output file /var/log/caddy/{host}.access.log { roll_keep 7 }Headerwww.mingongge.com { root * /web/mingongge.com/ file_server header Access-Control-Allow-Origin * header Cache-Control max-age=3600 header /css/* Cache-Control max-age=604800 }Cache-Control(cachecontrol) { header /css/* Cache-Control max-age=3600 header /img/* Cache-Control max-age=3600 header /js/* Cache-Control max-age=3600 }反向代理https://www.mingongge.com { tls admin@mingongge.com proxy / https://welcome.mingongge.com }快速创建一个站点caddy file-server --website ./index --listen :8088 --domain www.mingongge.com参数说明file-server #这是一个文件服务器 --website #文件服务的根目录 ./index #首页文件的存储目录(如存放 index.html) --listen :8088 #定义监听端口 --domain www.mingongge.com #绑定域名配置片段Caddyfile 支持类似代码中 function 一样的配置片段,这些配置片段可以在任意位置被 import,同时可以接受参数,以下为配置片断示例:# 括号内为片段名称,可以自行定义 (TLS) { protocols tls1.2 tls1.3 ciphers TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 # 在任意位置可以引用此片段从而达到配置复用 import TLS配置模块化import 指令除了支持引用配置片段以外,还支持引用外部文件,同时支持通配符,有了这个命令以后我们就可以方便的将配置文件进行模块化处理:# 引用外部的 /etc/caddy/*.caddy import /etc/caddy/*.caddy模块化配置实例CaddyfileCaddyfile 类似于 nginx 的 nginx.conf 主配置。 (LOG) { log { format formatted "[{ts}] {request>remote_addr} {request>proto} {request>method} <- {status} -> {request>host} {request>uri} {request>headers>User-Agent>[0]}" { time_format "iso8601" output file "{args.0}" { roll_size 100mb roll_keep 3 roll_keep_for 7d (TLS) { protocols tls1.2 tls1.3 ciphers TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (HSTS) { header / Strict-Transport-Security "max-age=63072000" (ACME_GANDI) { # 从环境变量获取 GANDI_API_TOKEN dns gandi {$GANDI_API_TOKEN} # 聚合上面的配置片段为新的片段 (COMMON_CONFIG) { # 压缩支持 encode zstd gzip # TLS 配置 tls { import TLS import ACME_GANDI # HSTS import HSTS # 开启 HTTP3 实验性支持 servers :443 { protocol { experimental_http3 # 引入其他具体的站点配置 import /etc/caddy/*.caddy站点配置www.mingongge.com { # 重定向到 mingongge.com(默认 302) redir https://mingongge.com{uri} import LOG "/data/logs/mingongge.com.log" # TLS、HSTS、ACME 等通用配置 import COMMON_CONFIG mingongge.com { route /* { reverse_proxy mingongge_com:80 import LOG "/data/logs/mingongge.com.log" # TLS、HSTS、ACME 等通用配置 import COMMON_CONFIG }配置完成后,通过 systemctl start caddy 去启动 caddy 服务器。每次配置修改后可以通过 systemctl reload caddy 进行配置重载。这些命令的功能与 Nginx 的命令功能基本一样。总结Caddy 内置了丰富的插件,例如 “file_server”、内置各种负载均衡策略等,这些插件组合在一起可以实现一些复杂的功能。Caddy 采用 Go 语言编写,相比较而言(Nginx 通过 Lua 或者 C 开发插件),对于后期的开发更容易上手。