分类: 未分类

  • 17 个方面,综合对比 Kafka、RabbitMQ、RocketMQ、ActiveMQ 四个分布式消息队列

    快乐分享,Java干货及时送达👇

    本文将从,Kafka、RabbitMQ、ZeroMQ、RocketMQ、ActiveMQ 17 个方面综合对比作为消息队列使用时的差异。

    1. 资料文档

    Kafka:中,有 kafka 作者自己写的书,网上资料也有一些。

    rabbitmq:多,有一些不错的书,网上资料多。

    zeromq:少,没有专门写 zeromq 的书,网上的资料多是一些代码的实现和简单介绍。

    rocketmq:少,没有专门写 rocketmq 的书,网上的资料良莠不齐,官方文档很简洁,但是对技术细节没有过多的描述。

    activemq:多,没有专门写 activemq 的书,网上资料多。

    2. 开发语言

    Kafka:Scala

    rabbitmq:Erlang

    zeromq:c

    rocketmq:java

    activemq:java

    3. 支持的协议

    Kafka:自己定义的一套…(基于 TCP)

    rabbitmq:AMQP

    zeromq:TCP、UDP

    rocketmq:自己定义的一套…

    activemq:OpenWire、STOMP、REST、XMPP、AMQP

    4. 消息存储

    Kafka:内存、磁盘、数据库。支持大量堆积。

    kafka 的最小存储单元是分区,一个 topic 包含多个分区,kafka 创建主题时,这些分区会被分配在多个服务器上,通常一个 broker 一台服务器。分区首领会均匀地分布在不同的服务器上,分区副本也会均匀的分布在不同的服务器上,确保负载均衡和高可用性,当新的 broker 加入集群的时候,部分副本会被移动到新的 broker 上。根据配置文件中的目录清单,kafka 会把新的分区分配给目录清单里分区数最少的目录。默认情况下,分区器使用轮询算法把消息均衡地分布在同一个主题的不同分区中,对于发送时指定了 key 的情况,会根据 key 的 hashcode 取模后的值存到对应的分区中。

    rabbitmq:内存、磁盘。支持少量堆积。

    rabbitmq 的消息分为持久化的消息和非持久化消息,不管是持久化的消息还是非持久化的消息都可以写入到磁盘。持久化的消息在到达队列时就写入到磁盘,并且如果可以,持久化的消息也会在内存中保存一份备份,这样可以提高一定的性能,当内存吃紧的时候会从内存中清除。非持久化的消息一般只存在于内存中,在内存吃紧的时候会被换入到磁盘中,以节省内存。

    引入镜像队列机制,可将重要队列“复制”到集群中的其他 broker 上,保证这些队列的消息不会丢失。配置镜像的队列,都包含一个主节点 master 和多个从节点 slave,如果 master 失效,加入时间最长的 slave 会被提升为新的 master,除发送消息外的所有动作都向 master 发送,然后由 master 将命令执行结果广播给各个 slave,rabbitmq 会让 master 均匀地分布在不同的服务器上,而同一个队列的 slave 也会均匀地分布在不同的服务器上,保证负载均衡和高可用性。

    zeromq:消息发送端的内存或者磁盘中。不支持持久化。

    rocketmq:磁盘。支持大量堆积。

    commitLog 文件存放实际的消息数据,每个 commitLog 上限是 1G,满了之后会自动新建一个 commitLog 文件保存数据。ConsumeQueue 队列只存放 offset、size、tagcode,非常小,分布在多个 broker 上。ConsumeQueue 相当于 CommitLog 的索引文件,消费者消费时会从 consumeQueue 中查找消息在 commitLog 中的 offset,再去 commitLog 中查找元数据。关注工众号:码猿技术专栏,回复关键词:1111 获取阿里内部Java性能调优手册!ConsumeQueue 存储格式的特性,保证了写过程的顺序写盘(写 CommitLog 文件),大量数据 IO 都在顺序写同一个 commitLog,满 1G 了再写新的。加上 rocketmq 是累计 4K 才强制从 PageCache 中刷到磁盘(缓存),所以高并发写性能突出。

    activemq:内存、磁盘、数据库。支持少量堆积。

    5. 消息事务

    Kafka:支持

    rabbitmq:支持。客户端将信道设置为事务模式,只有当消息被 rabbitMq 接收,事务才能提交成功,否则在捕获异常后进行回滚。使用事务会使得性能有所下降

    zeromq:不支持

    rocketmq:支持

    activemq:支持

    6. 负载均衡

    Kafka:支持负载均衡。

    1、一个 broker 通常就是一台服务器节点。对于同一个 Topic 的不同分区,Kafka 会尽力将这些分区分布到不同的 Broker 服务器上,zookeeper 保存了 broker、主题和分区的元数据信息。分区首领会处理来自客户端的生产请求,kafka 分区首领会被分配到不同的 broker 服务器上,让不同的 broker 服务器共同分担任务。

    每一个 broker 都缓存了元数据信息,客户端可以从任意一个 broker 获取元数据信息并缓存起来,根据元数据信息知道要往哪里发送请求。

    2、kafka 的消费者组订阅同一个 topic,会尽可能地使得每一个消费者分配到相同数量的分区,分摊负载。

    3、当消费者加入或者退出消费者组的时候,还会触发再均衡,为每一个消费者重新分配分区,分摊负载。

    kafka 的负载均衡大部分是自动完成的,分区的创建也是 kafka 完成的,隐藏了很多细节,避免了繁琐的配置和人为疏忽造成的负载问题。

    4、发送端由 topic 和 key 来决定消息发往哪个分区,如果 key 为 null,那么会使用轮询算法将消息均衡地发送到同一个 topic 的不同分区中。如果 key 不为 null,那么会根据 key 的 hashcode 取模计算出要发往的分区。

    rabbitmq:对负载均衡的支持不好。

    1、消息被投递到哪个队列是由交换器和 key 决定的,交换器、路由键、队列都需要手动创建。

    rabbitmq 客户端发送消息要和 broker 建立连接,需要事先知道 broker 上有哪些交换器,有哪些队列。通常要声明要发送的目标队列,如果没有目标队列,会在 broker 上创建一个队列,如果有,就什么都不处理,接着往这个队列发送消息。假设大部分繁重任务的队列都创建在同一个 broker 上,那么这个 broker 的负载就会过大。(可以在上线前预先创建队列,无需声明要发送的队列,但是发送时不会尝试创建队列,可能出现找不到队列的问题,rabbitmq 的备份交换器会把找不到队列的消息保存到一个专门的队列中,以便以后查询使用)

    使用镜像队列机制建立 rabbitmq 集群可以解决这个问题,形成 master-slave 的架构,master 节点会均匀分布在不同的服务器上,让每一台服务器分摊负载。slave 节点只是负责转发,在 master 失效时会选择加入时间最长的 slave 成为 master。

    当新节点加入镜像队列的时候,队列中的消息不会同步到新的 slave 中,除非调用同步命令,但是调用命令后,队列会阻塞,不能在生产环境中调用同步命令。

    2、当 rabbitmq 队列拥有多个消费者的时候,队列收到的消息将以轮询的分发方式发送给消费者。每条消息只会发送给订阅列表里的一个消费者,不会重复。

    这种方式非常适合扩展,而且是专门为并发程序设计的。

    如果某些消费者的任务比较繁重,那么可以设置 basicQos 限制信道上消费者能保持的最大未确认消息的数量,在达到上限时,rabbitmq 不再向这个消费者发送任何消息。

    3、对于 rabbitmq 而言,客户端与集群建立的 TCP 连接不是与集群中所有的节点建立连接,而是挑选其中一个节点建立连接。但是 rabbitmq 集群可以借助 HAProxy、LVS 技术,或者在客户端使用算法实现负载均衡,引入负载均衡之后,各个客户端的连接可以分摊到集群的各个节点之中。

    客户端均衡算法

    1. 轮询法。按顺序返回下一个服务器的连接地址。
    2. 加权轮询法。给配置高、负载低的机器配置更高的权重,让其处理更多的请求;而配置低、负载高的机器,给其分配较低的权重,降低其系统负载。
    3. 随机法。随机选取一个服务器的连接地址。
    4. 加权随机法。按照概率随机选取连接地址。
    5. 源地址哈希法。通过哈希函数计算得到的一个数值,用该数值对服务器列表的大小进行取模运算。
    6. 最小连接数法。动态选择当前连接数最少的一台服务器的连接地址。

    zeromq:去中心化,不支持负载均衡。本身只是一个多线程网络库。

    rocketmq:支持负载均衡。

    一个 broker 通常是一个服务器节点,broker 分为 master 和 slave,master 和 slave 存储的数据一样,slave 从 master 同步数据。

    1、nameserver 与每个集群成员保持心跳,保存着 Topic-Broker 路由信息,同一个 topic 的队列会分布在不同的服务器上。

    2、发送消息通过轮询队列的方式发送,每个队列接收平均的消息量。发送消息指定 topic、tags、keys,无法指定投递到哪个队列(没有意义,集群消费和广播消费跟消息存放在哪个队列没有关系)。

    tags 选填,类似于 Gmail 为每封邮件设置的标签,方便服务器过滤使用。目前只支 持每个消息设置一个 tag,所以也可以类比为 Notify 的 MessageType 概念。

    keys 选填,代表这条消息的业务关键词,服务器会根据 keys 创建哈希索引,设置后, 可以在 Console 系统根据 Topic、Keys 来查询消息,由于是哈希索引,请尽可能 保证 key 唯一,例如订单号,商品 Id 等。

    3、rocketmq 的负载均衡策略规定:Consumer 数量应该小于等于 Queue 数量,如果 Consumer 超过 Queue 数量,那么多余的 Consumer 将不能消费消息。这一点和 kafka 是一致的,rocketmq 会尽可能地为每一个 Consumer 分配相同数量的队列,分摊负载。

    activemq:支持负载均衡。可以基于 zookeeper 实现负载均衡。

    7. 集群方式

    Kafka:天然的‘Leader-Slave’无状态集群,每台服务器既是 Master 也是 Slave。

    分区首领均匀地分布在不同的 kafka 服务器上,分区副本也均匀地分布在不同的 kafka 服务器上,所以每一台 kafka 服务器既含有分区首领,同时又含有分区副本,每一台 kafka 服务器是某一台 kafka 服务器的 Slave,同时也是某一台 kafka 服务器的 leader。

    kafka 的集群依赖于 zookeeper,zookeeper 支持热扩展,所有的 broker、消费者、分区都可以动态加入移除,而无需关闭服务,与不依靠 zookeeper 集群的 mq 相比,这是最大的优势。

    rabbitmq:支持简单集群,’复制’模式,对高级集群模式支持不好。

    rabbitmq 的每一个节点,不管是单一节点系统或者是集群中的一部分,要么是内存节点,要么是磁盘节点,集群中至少要有一个是磁盘节点。

    在 rabbitmq 集群中创建队列,集群只会在单个节点创建队列进程和完整的队列信息(元数据、状态、内容),而不是在所有节点上创建。引入镜像队列,可以避免单点故障,确保服务的可用性,但是需要人为地为某些重要的队列配置镜像。

    zeromq:去中心化,不支持集群。

    rocketmq:常用 多对’Master-Slave’ 模式,开源版本需手动切换 Slave 变成 Master

    Name Server 是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。

    Broker 部署相对复杂,Broker 分为 Master 与 Slave,一个 Master 可以对应多个 Slave,但是一个 Slave 只能对应一个 Master,Master 与 Slave 的对应关系通过指定相同的 BrokerName,不同的 BrokerId 来定义,BrokerId 为 0 表示 Master,非 0 表示 Slave。Master 也可以部署多个。每个 Broker 与 Name Server 集群中的所有节点建立长连接,定时注册 Topic 信息到所有 Name Server。

    Producer 与 Name Server 集群中的其中一个节点(随机选择)建立长连接,定期从 Name Server 取 Topic 路由信息,并向提供 Topic 服务的 Master 建立长连接,且定时向 Master 发送心跳。Producer 完全无状态,可集群部署。

    Consumer 与 Name Server 集群中的其中一个节点(随机选择)建立长连接,定期从 Name Server 取 Topic 路由信息,并向提供 Topic 服务的 Master、Slave 建立长连接,且定时向 Master、Slave 发送心跳。Consumer 既可以从 Master 订阅消息,也可以从 Slave 订阅消息,订阅规则由 Broker 配置决定。

    客户端先找到 NameServer, 然后通过 NameServer 再找到 Broker。

    一个 topic 有多个队列,这些队列会均匀地分布在不同的 broker 服务器上。rocketmq 队列的概念和 kafka 的分区概念是基本一致的,kafka 同一个 topic 的分区尽可能地分布在不同的 broker 上,分区副本也会分布在不同的 broker 上。

    rocketmq 集群的 slave 会从 master 拉取数据备份,master 分布在不同的 broker 上。

    activemq:支持简单集群模式,比如’主-备’,对高级集群模式支持不好。

    8. 管理界面

    Kafka:一般

    rabbitmq:好

    zeromq:无

    rocketmq:无

    activemq:一般

    9. 可用性

    Kafka:非常高(分布式)

    rabbitmq:高(主从)

    zeromq:高

    rocketmq:非常高(分布式)

    activemq:高(主从)

    10. 消息重复

    Kafka:支持 at least once、at most once

    rabbitmq:支持 at least once、at most once

    zeromq:只有重传机制,但是没有持久化,消息丢了重传也没有用。既不是 at least once、也不是 at most once、更不是 exactly only once

    rocketmq:支持 at least once

    activemq:支持 at least once

    11. 吞吐量 TPS

    Kafka:极大 Kafka 按批次发送消息和消费消息。发送端将多个小消息合并,批量发向 Broker,消费端每次取出一个批次的消息批量处理。

    rabbitmq:比较大

    zeromq:极大

    rocketmq:大

    rocketMQ :接收端可以批量消费消息,可以配置每次消费的消息数,但是发送端不是批量发送。

    activemq:比较大

    12. 订阅形式和消息分发

    Kafka:基于 topic 以及按照 topic 进行正则匹配的发布订阅模式。

    【发送】

    发送端由 topic 和 key 来决定消息发往哪个分区,如果 key 为 null,那么会使用轮询算法将消息均衡地发送到同一个 topic 的不同分区中。如果 key 不为 null,那么会根据 key 的 hashcode 取模计算出要发往的分区。

    【接收】

    1、consumer 向群组协调器 broker 发送心跳来维持他们和群组的从属关系以及他们对分区的所有权关系,所有权关系一旦被分配就不会改变除非发生再均衡(比如有一个 consumer 加入或者离开 consumer group),consumer 只会从对应的分区读取消息。

    2、kafka 限制 consumer 个数要少于分区个数,每个消息只会被同一个 Consumer Group 的一个 consumer 消费(非广播)。

    3、kafka 的 Consumer Group 订阅同一个 topic,会尽可能地使得每一个 consumer 分配到相同数量的分区,不同 Consumer Group 订阅同一个主题相互独立,同一个消息会被不同的 Consumer Group 处理。

    rabbitmq:提供了 4 种:direct, topic ,Headers 和 fanout。

    【发送】

    先要声明一个队列,这个队列会被创建或者已经被创建,队列是基本存储单元。

    由 exchange 和 key 决定消息存储在哪个队列。

    direct:发送到和 bindingKey 完全匹配的队列。

    topic:路由 key 是含有”.”的字符串,会发送到含有“*”、“#”进行模糊匹配的 bingKey 对应的队列。

    fanout:与 key 无关,会发送到所有和 exchange 绑定的队列

    headers:与 key 无关,消息内容的 headers 属性(一个键值对)和绑定键值对完全匹配时,会发送到此队列。此方式性能低一般不用

    【接收】

    rabbitmq 的队列是基本存储单元,不再被分区或者分片,对于我们已经创建了的队列,消费端要指定从哪一个队列接收消息。

    当 rabbitmq 队列拥有多个消费者的时候,队列收到的消息将以轮询的分发方式发送给消费者。每条消息只会发送给订阅列表里的一个消费者,不会重复。

    这种方式非常适合扩展,而且是专门为并发程序设计的。

    如果某些消费者的任务比较繁重,那么可以设置 basicQos 限制信道上消费者能保持的最大未确认消息的数量,在达到上限时,rabbitmq 不再向这个消费者发送任何消息。

    zeromq:点对点(p2p)

    rocketmq:基于 topic/messageTag 以及按照消息类型、属性进行正则匹配的发布订阅模式

    【发送】

    发送消息通过轮询队列的方式发送,每个队列接收平均的消息量。发送消息指定 topic、tags、keys,无法指定投递到哪个队列(没有意义,集群消费和广播消费跟消息存放在哪个队列没有关系)。

    tags 选填,类似于 Gmail 为每封邮件设置的标签,方便服务器过滤使用。目前只支 持每个消息设置一个 tag,所以也可以类比为 Notify 的 MessageType 概念。

    keys 选填,代表这条消息的业务关键词,服务器会根据 keys 创建哈希索引,设置后, 可以在 Console 系统根据 Topic、Keys 来查询消息,由于是哈希索引,请尽可能 保证 key 唯一,例如订单号,商品 Id 等。

    【接收】

    1、广播消费。一条消息被多个 Consumer 消费,即使 Consumer 属于同一个 ConsumerGroup,消息也会被 ConsumerGroup 中的每个 Consumer 都消费一次。

    2、集群消费。一个 Consumer Group 中的 Consumer 实例平均分摊消费消息。例如某个 Topic 有 9 条消息,其中一个 Consumer Group 有 3 个实例,那么每个实例只消费其中的 3 条消息。即每一个队列都把消息轮流分发给每个 consumer。

    activemq:点对点(p2p)、广播(发布-订阅)

    点对点模式,每个消息只有 1 个消费者;

    发布/订阅模式,每个消息可以有多个消费者。

    【发送】

    点对点模式:先要指定一个队列,这个队列会被创建或者已经被创建。

    发布/订阅模式:先要指定一个 topic,这个 topic 会被创建或者已经被创建。

    【接收】

    点对点模式:对于已经创建了的队列,消费端要指定从哪一个队列接收消息。

    发布/订阅模式:对于已经创建了的 topic,消费端要指定订阅哪一个 topic 的消息。

    13. 顺序消息

    Kafka:支持。

    设置生产者的 max.in.flight.requests.per.connection 为 1,可以保证消息是按照发送顺序写入服务器的,即使发生了重试。kafka 保证同一个分区里的消息是有序的,但是这种有序分两种情况

    1、key 为 null,消息逐个被写入不同主机的分区中,但是对于每个分区依然是有序的

    2、key 不为 null , 消息被写入到同一个分区,这个分区的消息都是有序。

    rabbitmq:不支持

    zeromq:不支持

    rocketmq:支持

    activemq:不支持

    14. 消息确认

    Kafka:支持。

    1、发送方确认机制

    ack=0,不管消息是否成功写入分区

    ack=1,消息成功写入首领分区后,返回成功

    ack=all,消息成功写入所有分区后,返回成功。

    2、接收方确认机制

    自动或者手动提交分区偏移量,早期版本的 kafka 偏移量是提交给 Zookeeper 的,这样使得 zookeeper 的压力比较大,更新版本的 kafka 的偏移量是提交给 kafka 服务器的,不再依赖于 zookeeper 群组,集群的性能更加稳定。

    rabbitmq:支持。

    1、发送方确认机制,消息被投递到所有匹配的队列后,返回成功。如果消息和队列是可持久化的,那么在写入磁盘后,返回成功。支持批量确认和异步确认。

    2、接收方确认机制,设置 autoAck 为 false,需要显式确认,设置 autoAck 为 true,自动确认。

    当 autoAck 为 false 的时候,rabbitmq 队列会分成两部分,一部分是等待投递给 consumer 的消息,一部分是已经投递但是没收到确认的消息。如果一直没有收到确认信号,并且 consumer 已经断开连接,rabbitmq 会安排这个消息重新进入队列,投递给原来的消费者或者下一个消费者。

    未确认的消息不会有过期时间,如果一直没有确认,并且没有断开连接,rabbitmq 会一直等待,rabbitmq 允许一条消息处理的时间可以很久很久。

    zeromq:支持。

    rocketmq:支持。

    activemq:支持。

    15. 消息回溯

    Kafka:支持指定分区 offset 位置的回溯

    rabbitmq:不支持

    zeromq:不支持

    rocketmq:支持指定时间点的回溯

    activemq:不支持

    16. 消息重试

    Kafka:不支持,但是可以实现。

    kafka 支持指定分区 offset 位置的回溯,可以实现消息重试。

    rabbitmq:不支持,但是可以利用消息确认机制实现

    rabbitmq 接收方确认机制,设置 autoAck 为 false

    当 autoAck 为 false 的时候,rabbitmq 队列会分成两部分,一部分是等待投递给 consumer 的消息,一部分是已经投递但是没收到确认的消息。如果一直没有收到确认信号,并且 consumer 已经断开连接,rabbitmq 会安排这个消息重新进入队列,投递给原来的消费者或者下一个消费者。

    zeromq:不支持

    rocketmq:支持

    消息消费失败的大部分场景下,立即重试 99%都会失败,所以 rocketmq 的策略是在消费失败时定时重试,每次时间间隔相同。

    1、发送端的 send 方法本身支持内部重试,重试逻辑如下:

    a)至多重试 3 次;

    b)如果发送失败,则轮转到下一个 broker;

    c)这个方法的总耗时不超过 sendMsgTimeout 设置的值,默认 10s,超过时间不在重试。

    2、接收端。

    Consumer 消费消息失败后,要提供一种重试机制,令消息再消费一次。Consumer 消费消息失败通常可以分为以下两种情况:

    由于消息本身的原因,例如反序列化失败,消息数据本身无法处理(例如话费充值,当前消息的手机号被注销,无法充值)等。定时重试机制,比如过 10s 秒后再重试。

    由于依赖的下游应用服务不可用,例如 db 连接不可用,外系统网络不可达等。

    即使跳过当前失败的消息,消费其他消息同样也会报错。这种情况可以 sleep 30s,再消费下一条消息,减轻 Broker 重试消息的压力。

    activemq:不支持

    17. 并发度

    Kafka:高

    一个线程一个消费者,kafka 限制消费者的个数要小于等于分区数,如果要提高并行度,可以在消费者中再开启多线程,或者增加 consumer 实例数量。

    rabbitmq:极高

    本身是用 Erlang 语言写的,并发性能高。

    可在消费者中开启多线程,最常用的做法是一个 channel 对应一个消费者,每一个线程把持一个 channel,多个线程复用 connection 的 tcp 连接,减少性能开销。

    当 rabbitmq 队列拥有多个消费者的时候,队列收到的消息将以轮询的分发方式发送给消费者。每条消息只会发送给订阅列表里的一个消费者,不会重复。

    这种方式非常适合扩展,而且是专门为并发程序设计的。

    如果某些消费者的任务比较繁重,那么可以设置 basicQos 限制信道上消费者能保持的最大未确认消息的数量,在达到上限时,rabbitmq 不再向这个消费者发送任何消息。

    zeromq:高

    rocketmq:高

    1、rocketmq 限制消费者的个数少于等于队列数,但是可以在消费者中再开启多线程,这一点和 kafka 是一致的,提高并行度的方法相同。

    修改消费并行度方法

    a) 同一个 ConsumerGroup 下,通过增加 Consumer 实例数量来提高并行度,超过订阅队列数的 Consumer 实例无效。

    b) 提高单个 Consumer 的消费并行线程,通过修改参数 consumeThreadMin、consumeThreadMax

    2、同一个网络连接 connection,客户端多个线程可以同时发送请求,连接会被复用,减少性能开销。

    activemq:高

    单个 ActiveMQ 的接收和消费消息的速度在 1 万笔/秒(持久化 一般为 1-2 万, 非持久化 2 万以上),在生产环境中部署 10 个 Activemq 就能达到 10 万笔/秒以上的性能,部署越多的 activemq broker 在 MQ 上 latency 也就越低,系统吞吐量也就越高。

  • Jenkins 真得很牛逼!只是大部分人不会用而已~(保姆级教程)

    快乐分享,Java干货及时送达👇

    文章来源:https://zhangzhuo.ltd/articles/2022/06/04/1654333399919.html


    目录

    • 什么是流水线
    • 声明式流水线
    • Jenkinsfile 的使用


    什么是流水线


    jenkins 有 2 种流水线分为声明式流水线脚本化流水线,脚本化流水线是 jenkins 旧版本使用的流水线脚本,新版本 Jenkins 推荐使用声明式流水线。文档只介绍声明流水线。

    1、声明式流水线


    在声明式流水线语法中,流水线过程定义在 Pipeline{}中,Pipeline 块定义了整个流水线中完成的所有工作,比如

    参数说明:
    • agent any:在任何可用的代理上执行流水线或它的任何阶段,也就是执行流水线过程的位置,也可以指定到具体的节点
    • stage:定义流水线的执行过程(相当于一个阶段),比如下文所示的 Build、Test、Deploy, 但是这个名字是根据实际情况进行定义的,并非固定的名字
    • steps:执行某阶段具体的步骤。
    //Jenkinsfile (Declarative Pipeline)
    pipeline {
      agent any
        stages {
          stage('Build') {
            steps {
              echo 'Build'
            }
          }
          stage('Test') {
            steps {
              echo 'Test'
            }
          }
          stage('Deploy') {
            steps {
              echo 'Deploy'
          }
        }
      }
    }

    2、脚本化流水线


    在脚本化流水线语法中,会有一个或多个 Node(节点)块在整个流水线中执行核心工作

    参数说明:
    • node:在任何可用的代理上执行流水线或它的任何阶段,也可以指定到具体的节点
    • stage:和声明式的含义一致,定义流水线的阶段。Stage 块在脚本化流水线语法中是可选的,然而在脚本化流水线中实现 stage 块,可以清楚地在 Jenkins UI 界面中显示每个 stage 的任务子集。
    //Jenkinsfile (Scripted Pipeline)
    node {
      stage('Build') {
        echo 'Build'
      }
      stage('Test') {
        echo 'Test'
      }
      stage('Deploy') {
        echo 'Deploy'
      }
    }


    声明式流水线


    声明式流水线必须包含在一个 Pipeline 块中,比如是一个 Pipeline 块的格式

    pipeline {
      /* insert Declarative Pipeline here */
    }

    在声明式流水线中有效的基本语句和表达式遵循与 Groovy 的语法同样的规则,但有以下例外

    • 流水线顶层必须是一个 block,即 pipeline{}
    • 分隔符可以不需要分号,但是每条语句都必须在自己的行上
    • 块只能由 Sections、Directives、Steps 或 assignment statements 组成
    • 属性引用语句被当做是无参数的方法调用,比如 input 会被当做 input()。

    1、Sections


    声明式流水线中的 Sections 不是一个关键字或指令,而是包含一个或多个 Agent、Stages、 post、Directives 和 Steps 的代码区域块。

    1.1 Agent

    Agent 表示整个流水线或特定阶段中的步骤和命令执行的位置,该部分必须在 pipeline 块的顶层被定义,也可以在 stage 中再次定义,但是 stage 级别是可选的。

    any

    在任何可用的代理上执行流水线,配置语法

    pipeline {
      agent any
    }
    none

    表示该 Pipeline 脚本没有全局的 agent 配置。当顶层的 agent 配置为 none 时, 每个 stage 部分都需要包含它自己的 agent。配置语法

    pipeline {
      agent none
      stages {
        stage('Stage For Build'){
          agent any
        }
      }
    }
    label

    以节点标签形式选择某个具体的节点执行 Pipeline 命令,例如:agent { label 'my-defined-label' }。节点需要提前配置标签。

    pipeline {
      agent none
        stages {
          stage('Stage For Build'){
            agent { label 'role-master' }
            steps {
              echo "role-master"
            }
          }
        }
    }
    node

    和 label 配置类似,只不过是可以添加一些额外的配置,比如 customWorkspace(设置默认工作目录)

    pipeline {
      agent none
        stages {
          stage('Stage For Build'){
            agent {
              node {
                label 'role-master'
                customWorkspace "/tmp/zhangzhuo/data"
              }
            }
            steps {
              sh "echo role-master > 1.txt"
            }
          }
        }
    }
    dockerfile

    使用从源码中包含的 Dockerfile 所构建的容器执行流水线或 stage。此时对应的 agent 写法如下

    agent {
       dockerfile {
         filename 'Dockerfile.build'  //dockerfile文件名称
         dir 'build'                  //执行构建镜像的工作目录
         label 'role-master'          //执行的node节点,标签选择
         additionalBuildArgs '--build-arg version=1.0.2' //构建参数
       }
    }
    docker

    相当于 dockerfile,可以直接使用 docker 字段指定外部镜像即可,可以省去构建的时间。比如使用 maven 镜像进行打包,同时可以指定 args

    agent{
      docker{
        image '192.168.10.15/kubernetes/alpine:latest'   //镜像地址
        label 'role-master' //执行的节点,标签选择
        args '-v /tmp:/tmp'      //启动镜像的参数
      }
    }
    kubernetes

    需要部署 kubernetes 相关的插件,官方文档:

    https://github.com/jenkinsci/kubernetes-plugin/

    Jenkins 也支持使用 Kubernetes 创建 Slave,也就是常说的动态 Slave。配置示例如下

    • cloud: Configure Clouds 的名称,指定到其中一个 k8s

    • slaveConnectTimeout: 连接超时时间

    • yaml: pod 定义文件,jnlp 容器的配置必须有配置无需改变,其余 containerd 根据自己情况指定

    • workspaceVolume:持久化 jenkins 的工作目录。

      • persistentVolumeClaimWorkspaceVolume:挂载已有 pvc。
    workspaceVolume persistentVolumeClaimWorkspaceVolume(claimName: "jenkins-agent", mountPath: "/", readOnly: "false")
    • nfsWorkspaceVolume:挂载 nfs 服务器目录
    workspaceVolume nfsWorkspaceVolume(serverAddress: "192.168.10.254", serverPath: "/nfs", readOnly: "false")
    • dynamicPVC:动态申请 pvc,任务执行结束后删除
    workspaceVolume dynamicPVC(storageClassName: "nfs-client", requestsSize: "1Gi", accessModes: "ReadWriteMany")
    • emptyDirWorkspaceVolume:临时目录,任务执行结束后会随着 pod 删除被删除,主要功能多个任务 container 共享 jenkins 工作目录。
    workspaceVolume emptyDirWorkspaceVolume()
    • hostPathWorkspaceVolume:挂载 node 节点本机目录,注意挂载本机目录注意权限问题,可以先创建设置 777 权限,否则默认 kubelet 创建的目录权限为 755 默认其他用户没有写权限,执行流水线会报错。
    workspaceVolume hostPathWorkspaceVolume(hostPath: "/opt/workspace", readOnly: false)
    示例
    agent {
      kubernetes {
          cloud 'kubernetes'
          slaveConnectTimeout 1200
          workspaceVolume emptyDirWorkspaceVolume()
          yaml '''
    kind: Pod
    metadata:
      name: jenkins-agent
    spec:
      containers:
      - args: ['$(JENKINS_SECRET)', '$(JENKINS_NAME)']
        image: '192.168.10.15/kubernetes/jnlp:alpine'
        name: jnlp
        imagePullPolicy: IfNotPresent
      - command:
          - "cat"
        image: "192.168.10.15/kubernetes/alpine:latest"
        imagePullPolicy: "IfNotPresent"
        name: "date"
        tty: true
      restartPolicy: Never
    '''
      }
    }
    1.2 agent 的配置示例

    kubernetes 示例

    pipeline {
      agent {
        kubernetes {
          cloud 'kubernetes'
          slaveConnectTimeout 1200
          workspaceVolume emptyDirWorkspaceVolume()
          yaml '''
    kind: Pod
    metadata:
      name: jenkins-agent
    spec:
      containers:
      - args: ['$(JENKINS_SECRET)', '$(JENKINS_NAME)']
        image: '192.168.10.15/kubernetes/jnlp:alpine'
        name: jnlp
        imagePullPolicy: IfNotPresent
      - command:
          - "cat"
        image: "192.168.10.15/kubernetes/alpine:latest"
        imagePullPolicy: "IfNotPresent"
        name: "date"
        tty: true
      - command:
          - "cat"
        image: "192.168.10.15/kubernetes/kubectl:apline"
        imagePullPolicy: "IfNotPresent"
        name: "kubectl"
        tty: true
      restartPolicy: Never
    '''
        }
      }
      environment 
    {
        MY_KUBECONFIG = credentials('kubernetes-cluster')
      }
      stages {
        stage('Data') {
          steps {
            container(name: 'date') {
              sh """
                date
              "
    ""
            }
          }
        }
        stage('echo') {
          steps {
            container(name: 'date') {
              sh """
                echo 'k8s is pod'
              "
    ""
            }
          }
        }
        stage('kubectl') {
          steps {
            container(name: 'kubectl') {
              sh """
                kubectl get pod -A  --kubeconfig $MY_KUBECONFIG
              "
    ""
            }
          }
        }
      }
    }

    docker 的示例

    pipeline {
      agent none
      stages {
        stage('Example Build') {
          agent { docker 'maven:3-alpine' }
          steps {
            echo 'Hello, Maven'
            sh 'mvn --version'
          }
        }
        stage('Example Test') {
          agent { docker 'openjdk:8-jre' }
          steps {
            echo 'Hello, JDK'
            sh 'java -version'
          }
        }
      }
    }
    1.3 Post

    Post 一般用于流水线结束后的进一步处理,比如错误通知等。Post 可以针对流水线不同的结果做出不同的处理,就像开发程序的错误处理,比如 Python 语言的 try catch。

    Post 可以定义在 Pipeline 或 stage 中,目前支持以下条件

    • always:无论 Pipeline 或 stage 的完成状态如何,都允许运行该 post 中定义的指令;
    • changed:只有当前 Pipeline 或 stage 的完成状态与它之前的运行不同时,才允许在该 post 部分运行该步骤;
    • fixed:当本次 Pipeline 或 stage 成功,且上一次构建是失败或不稳定时,允许运行该 post 中定义的指令;
    • regression:当本次 Pipeline 或 stage 的状态为失败、不稳定或终止,且上一次构建的 状态为成功时,允许运行该 post 中定义的指令;
    • failure:只有当前 Pipeline 或 stage 的完成状态为失败(failure),才允许在 post 部分运行该步骤,通常这时在 Web 界面中显示为红色
    • success:当前状态为成功(success),执行 post 步骤,通常在 Web 界面中显示为蓝色 或绿色
    • unstable:当前状态为不稳定(unstable),执行 post 步骤,通常由于测试失败或代码 违规等造成,在 Web 界面中显示为黄色
    • aborted:当前状态为终止(aborted),执行该 post 步骤,通常由于流水线被手动终止触发,这时在 Web 界面中显示为灰色;
    • unsuccessful:当前状态不是 success 时,执行该 post 步骤;
    • cleanup:无论 pipeline 或 stage 的完成状态如何,都允许运行该 post 中定义的指令。和 always 的区别在于,cleanup 会在其它执行之后执行。
    示例

    一般情况下 post 部分放在流水线的底部,比如本实例,无论 stage 的完成状态如何,都会输出一条 I will always say Hello again!信息

    //Jenkinsfile (Declarative Pipeline)
    pipeline {
      agent any
      stages {
        stage('Example1') {
          steps {
            echo 'Hello World1'
          }
        }
        stage('Example2') {
          steps {
            echo 'Hello World2'
          }
        }
      }
      post {
        always {
          echo 'I will always say Hello again!'
        }
      }
    }

    也可以将 post 写在 stage,下面示例表示 Example1 执行失败执行 post。

    //Jenkinsfile (Declarative Pipeline)
    pipeline {
      agent any
      stages {
        stage('Example1') {
          steps {
            sh 'ip a'
          }
          post {
            failure {
              echo 'I will always say Hello again!'
            }
          }
        }
      }
    }
    1.4 sepes

    Steps 部分在给定的 stage 指令中执行的一个或多个步骤,比如在 steps 定义执行一条 shell 命令

    //Jenkinsfile (Declarative Pipeline)
    pipeline {
      agent any
      stages {
        stage('Example') {
          steps {
            echo 'Hello World'
          }
        }
      }
    }

    或者是使用 sh 字段执行多条指令

    //Jenkinsfile (Declarative Pipeline)
    pipeline {
      agent any
      stages {
        stage('Example') {
          steps {
            sh """
               echo 'Hello World1'
               echo 'Hello World2'
            "
    ""
          }
        }
      }
    }

    2、Directives


    Directives 可用于一些执行 stage 时的条件判断或预处理一些数据,和 Sections 一致,Directives 不是一个关键字或指令,而是包含了 environment、options、parameters、triggers、stage、tools、 input、when 等配置。

    2.1 Environment

    Environment 主要用于在流水线中配置的一些环境变量,根据配置的位置决定环境变量的作用域。可以定义在 pipeline 中作为全局变量,也可以配置在 stage 中作为该 stage 的环境变量。该指令支持一个特殊的方法 credentials(),该方法可用于在 Jenkins 环境中通过标识符访问预定义的凭证。对于类型为 Secret Text 的凭证,credentials()可以将该 Secret 中的文本内容赋值给环境变量。对于类型为标准的账号密码型的凭证,指定的环境变量为 username 和 password,并且也会定义两个额外的环境变量,分别为MYVARNAME_USR和MYVARNAME_PSW。

    基本变量使用

    //示例
    pipeline {
      agent any
      environment {   //全局变量,会在所有stage中生效
        NAME= 'zhangzhuo'
      }
      stages {
        stage('env1') {
          environment { //定义在stage中的变量只会在当前stage生效,其他的stage不会生效
            HARBOR = 'https://192.168.10.15'
          }
          steps {
            sh "env"
          }
        }
        stage('env2') {
          steps {
            sh "env"
          }
        }
      }
    }

    使用变量引用 secret 的凭证

    //这里使用k8s的kubeconfig文件示例
    pipeline {
      agent any
      environment {
        KUBECONFIG = credentials('kubernetes-cluster')
      }
      stages {
        stage('env') {
          steps {
            sh "env"  //默认情况下输出的变量内容会被加密
          }
        }
      }
    }
    使用变量引用类型为标准的账号密码型的凭证

    这里使用 HARBOR 变量进行演示,默认情况下账号密码型的凭证会自动创建 3 个变量

    • HARBOR_USR:会把凭证中 username 值赋值给这个变量
    • HARBOR_PSW:会把凭证中 password 值赋值给这个变量
    • HARBOR:默认情况下赋值的值为usernamme:password
    //这里使用k8s的kubeconfig文件示例
    pipeline {
      agent any
      environment {
        HARBOR = credentials('harbor-account')
      }
      stages {
        stage('env') {
          steps {
            sh "env"
          }
        }
      }
    }
    2.2 Options

    Jenkins 流水线支持很多内置指令,比如 retry 可以对失败的步骤进行重复执行 n 次,可以根据不同的指令实现不同的效果。比较常用的指令如下:

    • buildDiscarder :保留多少个流水线的构建记录
    • disableConcurrentBuilds:禁止流水线并行执行,防止并行流水线同时访问共享资源导致流水线失败。
    • disableResume :如果控制器重启,禁止流水线自动恢复。
    • newContainerPerStage:agent 为 docker 或 dockerfile 时,每个阶段将在同一个节点的新容器中运行,而不是所有的阶段都在同一个容器中运行。
    • quietPeriod:流水线静默期,也就是触发流水线后等待一会在执行。
    • retry:流水线失败后重试次数。
    • timeout:设置流水线的超时时间,超过流水线时间,job 会自动终止。如果不加 unit 参数默认为 1 分。
    • timestamps:为控制台输出时间戳。

    定义在 pipeline 中

    pipeline {
      agent any
      options {
        timeout(time: 1, unit: 'HOURS')  //超时时间1小时,如果不加unit参数默认为1分
        timestamps()                     //所有输出每行都会打印时间戳
        buildDiscarder(logRotator(numToKeepStr: '3')) //保留三个历史构建版本
        quietPeriod(10)  //注意手动触发的构建不生效
        retry(3)    //流水线失败后重试次数
      }
      stages {
        stage('env1') {
          steps {
            sh "env"
            sleep 2
          }
        }
        stage('env2') {
          steps {
            sh "env"
          }
        }
      }
    }

    定义在 stage 中

    Option 除了写在 Pipeline 顶层,还可以写在 stage 中,但是写在 stage 中的 option 仅支持 retry、 timeout、timestamps,或者是和 stage 相关的声明式选项,比如 skipDefaultCheckout。处于 stage 级别的 options 写法如下

    pipeline {
      agent any
      stages {
        stage('env1') {
          options {   //定义在这里这对这个stage生效
            timeout(time: 2, unit: 'SECONDS'//超时时间2秒
            timestamps()                     //所有输出每行都会打印时间戳
            retry(3)    //流水线失败后重试次数
          }
          steps {
            sh "env && sleep 2"
          }
        }
        stage('env2') {
          steps {
            sh "env"
          }
        }
      }
    }
    2.3 Parameters

    Parameters 提供了一个用户在触发流水线时应该提供的参数列表,这些用户指定参数的值可以通过 params 对象提供给流水线的 step(步骤)。只能定义在 pipeline 顶层。

    目前支持的参数类型如下
    • string:字符串类型的参数。
    • text:文本型参数,一般用于定义多行文本内容的变量。
    • booleanParam:布尔型参数。
    • choice:选择型参数,一般用于给定几个可选的值,然后选择其中一个进行赋值。
    • password:密码型变量,一般用于定义敏感型变量,在 Jenkins 控制台会输出为*。
    插件 Parameters
    • imageTag:镜像 tag,需要安装 Image Tag Parameter 插件后使用
    • gitParameter:获取 git 仓库分支,需要 Git Parameter 插件后使用
    示例
    pipeline {
      agent any
      parameters {
        string(name: 'DEPLOY_ENV', defaultValue:  'staging', description: '1')   //执行构建时需要手动配置字符串类型参数,之后赋值给变量
        text(name:  'DEPLOY_TEXT', defaultValue: 'OnenTwonThreen', description: '2')  //执行构建时需要提供文本参数,之后赋值给变量
        booleanParam(name: 'DEBUG_BUILD',  defaultValue: true, description: '3')   //布尔型参数
        choice(name: 'CHOICES', choices: ['one''two''three'], description: '4')  //选择形式列表参数
        password(name: 'PASSWORD', defaultValue: 'SECRET', description: 'A  secret password')  //密码类型参数,会进行加密
        imageTag(name: 'DOCKER_IMAGE', description: '', image: 'kubernetes/kubectl', filter: '.*', defaultTag: '', registry: 'https://192.168.10.15', credentialId: 'harbor-account', tagOrder: 'NATURAL')   //获取镜像名称与tag
        gitParameter(branch: '', branchFilter: 'origin/(.*)', defaultValue: '', description: 'Branch for build and deploy', name: 'BRANCH', quickFilterEnabled: false, selectedValue: 'NONE', sortMode: 'NONE',  tagFilter: '*', type: 'PT_BRANCH')
      }  //获取git仓库分支列表,必须有git引用
      stages {
        stage('env1') {
          steps {
            sh "env"
          }
        }
        stage('git') {
          steps {
            git branch: "$BRANCH", credentialsId: 'gitlab-key', url: 'git@192.168.10.14:root/env.git'   //使用gitParameter,必须有这个
          }
        }
      }
    }
    2.4 Triggers

    在 Pipeline 中可以用 triggers 实现自动触发流水线执行任务,可以通过 Webhook、Cron、 pollSCM 和 upstream 等方式触发流水线。

    Cron

    定时构建假如某个流水线构建的时间比较长,或者某个流水线需要定期在某个时间段执行构建,可以 使用 cron 配置触发器,比如周一到周五每隔四个小时执行一次

    注意:H 的意思不是 HOURS 的意思,而是 Hash 的缩写。主要为了解决多个流水线在同一时间同时运行带来的系统负载压力。

    pipeline {
      agent any
      triggers {
        cron('H */4 * * 1-5')   //周一到周五每隔四个小时执行一次
        cron('H/12 * * * *')   //每隔12分钟执行一次
        cron('H * * * *')   //每隔1小时执行一次
      }
      stages {
        stage('Example') {
          steps {
            echo 'Hello World'
          }
        }
      }
    }
    Upstream

    Upstream 可以根据上游 job 的执行结果决定是否触发该流水线。比如当 job1 或 job2 执行成功时触发该流水线

    目前支持的状态有 SUCCESSUNSTABLEFAILURENOT_BUILTABORTED 等。

    pipeline {
      agent any
      triggers {
        upstream(upstreamProjects: 'env', threshold: hudson.model.Result.SUCCESS)  //当env构建成功时构建这个流水线
      }
      stages {
        stage('Example') {
          steps {
            echo 'Hello World'
          }
        }
      }
    }
    2.5 Input

    Input 字段可以实现在流水线中进行交互式操作,比如选择要部署的环境、是否继续执行某个阶段等。

    配置 Input 支持以下选项
    • message:必选,需要用户进行 input 的提示信息,比如:“是否发布到生产环境?”;
    • id:可选,input 的标识符,默认为 stage 的名称;
    • ok:可选,确认按钮的显示信息,比如:“确定”、“允许”;
    • submitter:可选,允许提交 input 操作的用户或组的名称,如果为空,任何登录用户均可提交 input;
    • parameters:提供一个参数列表供 input 使用。

    假如需要配置一个提示消息为“还继续么”、确认按钮为“继续”、提供一个 PERSON 的变量的参数,并且只能由登录用户为 alice 和 bob 提交的 input 流水线

    pipeline {
      agent any
      stages {
        stage('Example') {
          input {
            message "还继续么?"
            ok "继续"
            submitter "alice,bob"
            parameters {
              string(name: 'PERSON', defaultValue: 'Mr Jenkins', description: 'Who should I say hello to?')
            }
          }
          steps {
            echo "Hello, ${PERSON}, nice to meet you."
          }
        }
      }
    }
    2.6 when

    When 指令允许流水线根据给定的条件决定是否应该执行该 stage,when 指令必须包含至少 一个条件。如果 when 包含多个条件,所有的子条件必须都返回 True,stage 才能执行。

    When 也可以结合 not、allOf、anyOf 语法达到更灵活的条件匹配。

    目前比较常用的内置条件如下
    • branch:当正在构建的分支与给定的分支匹配时,执行这个 stage。注意,branch 只适用于多分支流水线
    • changelog:匹配提交的 changeLog 决定是否构建,例如:when { changelog '.*^\[DEPENDENCY\] .+$' }
    • environment:当指定的环境变量和给定的变量匹配时,执行这个 stage,例如:when { environment name: 'DEPLOY_TO', value: 'production' }
    • equals:当期望值和实际值相同时,执行这个 stage,例如:when { equals expected: 2, actual: currentBuild.number }
    • expression:当指定的 Groovy 表达式评估为 True,执行这个 stage,例如:when { expression { return params.DEBUG_BUILD } }
    • tag:如果 TAG_NAME 的值和给定的条件匹配,执行这个 stage,例如:when { tag "release-" }
    • not:当嵌套条件出现错误时,执行这个 stage,必须包含一个条件,例如:when { not { branch 'master' } }
    • allOf:当所有的嵌套条件都正确时,执行这个 stage,必须包含至少一个条件,例如:when { allOf { branch 'master'; environment name: 'DEPLOY_TO', value: 'production' } }
    • anyOf:当至少有一个嵌套条件为 True 时,执行这个 stage,例如:when { anyOf { branch 'master'; branch 'staging' } }

    示例:当分支为 main 时执行 Example Deploy 步骤

    pipeline {
      agent any
      stages {
        stage('Example Build') {
          steps {
            echo 'Hello World'
          }
        }
        stage('Example Deploy') {
          when {
            branch 'main' //多分支流水线,分支为才会执行。
          }
          steps {
            echo 'Deploying'
          }
        }
      }
    }

    也可以同时配置多个条件,比如分支是 production,而且 DEPLOY_TO 变量的值为 main 时,才执行 Example Deploy

    pipeline {
      agent any
      environment {
        DEPLOY_TO = "main"
      }
      stages {
        stage('Example Deploy') {
          when {
            branch 'main'
            environment name: 'DEPLOY_TO', value: 'main'
          }
          steps {
            echo 'Deploying'
          }
        }
      }
    }

    也可以使用 anyOf 进行匹配其中一个条件即可,比如分支为 main 或 DEPLOY_TO 为 main 或 master 时执行 Deploy

    pipeline {
      agent any
      stages {
        stage('Example Deploy') {
          when {
            anyOf {
              branch 'main'
              environment name: 'DEPLOY_TO', value: 'main'
              environment name: 'DEPLOY_TO', value: 'master'
            }
          }
          steps {
            echo 'Deploying'
          }
        }
      }
    }

    也可以使用 expression 进行正则匹配,比如当 BRANCH_NAME 为 main 或 master,并且 DEPLOY_TO 为 master 或 main 时才会执行 Example Deploy

    pipeline {
      agent any
      stages {
        stage('Example Deploy') {
          when {
            expression { BRANCH_NAME ==~ /(main|master)/ }
            anyOf {
              environment name: 'DEPLOY_TO', value: 'main'
              environment name: 'DEPLOY_TO', value: 'master'
            }
          }
          steps {
            echo 'Deploying'
          }
        }
      }
    }

    默认情况下,如果定义了某个 stage 的 agent,在进入该 stage 的 agent 后,该 stage 的 when 条件才会被评估,但是可以通过一些选项更改此选项。比如在进入 stage 的 agent 前评估 when, 可以使用 beforeAgent,当 when 为 true 时才进行该 stage

    目前支持的前置条件如下
    • beforeAgent:如果 beforeAgent 为 true,则会先评估 when 条件。在 when 条件为 true 时,才会进入该 stage
    • beforeInput:如果 beforeInput 为 true,则会先评估 when 条件。在 when 条件为 true 时,才会进入到 input 阶段;
    • beforeOptions:如果 beforeInput 为 true,则会先评估 when 条件。在 when 条件为 true 时,才会进入到 options 阶段;
    • beforeOptions 优先级大于 beforeInput 大于 beforeAgent

    示例

    pipeline {
      agent none
      stages {
        stage('Example Build') {
          steps {
            echo 'Hello World'
          }
        }
        stage('Example Deploy') {
          when {
            beforeAgent true
            branch 'main'
          }
          steps {
            echo 'Deploying'
          }
        }
      }
    }

    3、Parallel


    在声明式流水线中可以使用 Parallel 字段,即可很方便的实现并发构建,比如对分支 A、B、 C 进行并行处理

    pipeline {
      agent any
      stages {
        stage('Non-Parallel Stage') {
          steps {
            echo 'This stage will be executed first.'
          }
        }
        stage('Parallel Stage') {
          failFast true         //表示其中只要有一个分支构建执行失败,就直接推出不等待其他分支构建
          parallel {
            stage('Branch A') {
              steps {
                echo "On Branch A"
              }
            }
            stage('Branch B') {
              steps {
                echo "On Branch B"
              }
            }
            stage('Branch C') {
              stages {
                stage('Nested 1') {
                  steps {
                    echo "In stage Nested 1 within Branch C"
                  }
                }
                stage('Nested 2') {
                  steps {
                   echo "In stage Nested 2 within Branch C"
                  }
                }
              }
            }
          }
        }
      }
    }


    Jenkinsfile 的使用


    上面讲过流水线支持两种语法,即声明式和脚本式,这两种语法都支持构建持续交付流水线。并且都可以用来在 Web UI 或 Jenkinsfile 中定义流水线,不过通常将 Jenkinsfile 放置于代码仓库中(当然也可以放在单独的代码仓库中进行管理)。

    创建一个 Jenkinsfile 并将其放置于代码仓库中,有以下好处

    • 方便对流水线上的代码进行复查/迭代
    • 对管道进行审计跟踪
    • 流水线真正的源代码能够被项目的多个成员查看和编辑

    1、环境变量


    1.1 静态变量

    Jenkins 有许多内置变量可以直接在 Jenkinsfile 中使用,可以通过 JENKINS_URL/pipeline/syntax/globals#env 获取完整列表。目前比较常用的环境变量如下

    • BUILD_ID:当前构建的 ID,与 Jenkins 版本 1.597+中的 BUILD_NUMBER 完全相同
    • BUILD_NUMBER:当前构建的 ID,和 BUILD_ID 一致
    • BUILD_TAG:用来标识构建的版本号,格式为:jenkins-{BUILD_NUMBER}, 可以对产物进行命名,比如生产的 jar 包名字、镜像的 TAG 等;
    • BUILD_URL:本次构建的完整 URL,比如:http://buildserver/jenkins/job/MyJobName/17/%EF%BC%9B
    • JOB_NAME:本次构建的项目名称
    • NODE_NAME:当前构建节点的名称;
    • JENKINS_URL:Jenkins 完整的 URL,需要在 SystemConfiguration 设置;
    • WORKSPACE:执行构建的工作目录。

    示例如果一个流水线名称为print_env,第 2 次构建,各个变量的值。

    BUILD_ID:2
    BUILD_NUMBER:2
    BUILD_TAG:jenkins-print_env-2
    BUILD_URL:http://192.168.10.16:8080/job/print_env/2/
    JOB_NAME:print_env
    NODE_NAME:built-in
    JENKINS_URL:http://192.168.10.16:8080/
    WORKSPACE:/bitnami/jenkins/home/workspace/print_env

    上述变量会保存在一个 Map 中,可以使用 env.BUILD_ID 或 env.JENKINS_URL 引用某个内置变量

    pipeline {
      agent any
      stages {
        stage('print env') {
          parallel {
            stage('BUILD_ID') {
              steps {
                echo "$env.BUILD_ID"
              }
            }
            stage('BUILD_NUMBER') {
              steps {
                echo "$env.BUILD_NUMBER"
              }
            }
            stage('BUILD_TAG') {
              steps {
                echo "$env.BUILD_TAG"
              }
            }
          }
        }
      }
    }
    1.2 动态变量

    动态变量是根据某个指令的结果进行动态赋值,变量的值根据指令的执行结果而不同。如下所示

    • returnStdout:将命令的执行结果赋值给变量,比如下述的命令返回的是 clang,此时 CC 的值为“clang”。
    • returnStatus:将命令的执行状态赋值给变量,比如下述命令的执行状态为 1,此时 EXIT_STATUS 的值为 1。
    //Jenkinsfile (Declarative Pipeline)
    pipeline {
      agent any
      environment {
        // 使用 returnStdout
        CC = """${sh(
             returnStdout: true,
             script: 'echo -n "
    clang"'   //如果使用shell命令的echo赋值变量最好加-n取消换行
             )}"
    ""
        // 使用 returnStatus
        EXIT_STATUS = """${sh(
             returnStatus: true,
             script: 'exit 1'
             )}"
    ""
      }
      stages {
        stage('Example') {
          environment {
            DEBUG_FLAGS = '-g'
          }
          steps {
            sh 'printenv'
          }
        }
      }
    }

    2、凭证管理


    Jenkins 的声明式流水线语法有一个 credentials()函数,它支持 secret text(加密文本)、username 和 password(用户名和密码)以及 secret file(加密文件)等。接下来看一下一些常用的凭证处理方法。

    2.1 加密文本

    本实例演示将两个 Secret 文本凭证分配给单独的环境变量来访问 Amazon Web 服务,需要 提前创建这两个文件的 credentials(实践的章节会有演示),Jenkinsfile 文件的内容如下

    //Jenkinsfile (Declarative Pipeline)
    pipeline {
      agent any
      environment {
        AWS_ACCESS_KEY_ID = credentials('txt1')
        AWS_SECRET_ACCESS_KEY = credentials('txt2')
      }
      stages {
        stage('Example stage 1') {
          steps {
            echo "$AWS_ACCESS_KEY_ID"
          }
        }
        stage('Example stage 2') {
          steps {
            echo "$AWS_SECRET_ACCESS_KEY"
          }
        }
      }
    }
    2.2 用户名密码

    本示例用来演示 credentials 账号密码的使用,比如使用一个公用账户访问 Bitbucket、GitLab、 Harbor 等。假设已经配置完成了用户名密码形式的 credentials,凭证 ID 为 harbor-account

    //Jenkinsfile (Declarative Pipeline)
    pipeline {
      agent any
      environment {
        BITBUCKET_COMMON_CREDS = credentials('harbor-account')
      }
      stages {
        stage('printenv') {
          steps {
            sh "env"
          }
        }
    }

    上述的配置会自动生成 3 个环境变量

    • BITBUCKET_COMMON_CREDS:包含一个以冒号分隔的用户名和密码,格式为 username:password
    • BITBUCKET_COMMON_CREDS_USR:仅包含用户名的附加变量
    • BITBUCKET_COMMON_CREDS_PSW:仅包含密码的附加变量。
    2.3 加密文件

    需要加密保存的文件,也可以使用 credential,比如链接到 Kubernetes 集群的 kubeconfig 文件等。

    假如已经配置好了一个 kubeconfig 文件,此时可以在 Pipeline 中引用该文件

    //Jenkinsfile (Declarative Pipeline)
    pipeline {
      agent {
        kubernetes {
          cloud 'kubernetes'
          slaveConnectTimeout 1200
          workspaceVolume emptyDirWorkspaceVolume()
          yaml '''
    kind: Pod
    metadata:
      name: jenkins-agent
    spec:
      containers:
      - args: ['$(JENKINS_SECRET)', '$(JENKINS_NAME)']
        image: '192.168.10.15/kubernetes/jnlp:alpine'
        name: jnlp
        imagePullPolicy: IfNotPresent
      - command:
          - "cat"
        image: "192.168.10.15/kubernetes/kubectl:apline"
        imagePullPolicy: "IfNotPresent"
        name: "kubectl"
        tty: true
      restartPolicy: Never
    '''
        }
      }
      environment 
    {
        MY_KUBECONFIG = credentials('kubernetes-cluster')
      }
      stages {
        stage('kubectl') {
          steps {
            container(name: 'kubectl') {
              sh """
                kubectl get pod -A  --kubeconfig $MY_KUBECONFIG
              "
    ""
            }
          }
        }
      }
    }

  • 九种分布式ID解决方案,总有一款适合你!

    快乐分享,Java干货及时送达👇

    • 1、UUID
    • 2、数据库自增ID
      • 2.1、主键表
      • 2.2、ID自增步长设置
    • 3、号段模式
    • 4、Redis INCR
    • 5、雪花算法
    • 6、美团(Leaf)
    • 7、百度(Uidgenerator)
    • 8、滴滴(TinyID)
    • 总结比较

    背景

    在复杂的分布式系统中,往往需要对大量的数据进行唯一标识,比如在对一个订单表进行了分库分表操作,这时候数据库的自增ID显然不能作为某个订单的唯一标识。除此之外还有其他分布式场景对分布式ID的一些要求:

    • 趋势递增: 由于多数RDBMS使用B-tree的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能。
    • 单调递增: 保证下一个ID一定大于上一个ID,例如排序需求。
    • 信息安全: 如果ID是连续的,恶意用户的扒取工作就非常容易做了;如果是订单号就更危险了,可以直接知道我们的单量。所以在一些应用场景下,会需要ID无规则、不规则。

    就不同的场景及要求,市面诞生了很多分布式ID解决方案。本文针对多个分布式ID解决方案进行介绍,包括其优缺点、使用场景及代码示例。

    1、UUID

    UUID(Universally Unique Identifier)是基于当前时间、计数器(counter)和硬件标识(通常为无线网卡的MAC地址)等数据计算生成的。包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符,可以生成全球唯一的编码并且性能高效。

    JDK提供了UUID生成工具,代码如下:

    import java.util.UUID;

    public class Test {
        public static void main(String[] args) {
            System.out.println(UUID.randomUUID());
        }
    }

    输出如下

    b0378f6a-eeb7-4779-bffe-2a9f3bc76380

    UUID完全可以满足分布式唯一标识,但是在实际应用过程中一般不采用,有如下几个原因:

    • 存储成本高: UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。
    • 信息不安全: 基于MAC地址生成的UUID算法会暴露MAC地址,曾经梅丽莎病毒的制造者就是根据UUID寻找的。
    • 不符合MySQL主键要求: MySQL官方有明确的建议主键要尽量越短越好,因为太长对MySQL索引不利:如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能。

    2、数据库自增ID

    利用Mysql的特性ID自增,可以达到数据唯一标识,但是分库分表后只能保证一个表中的ID的唯一,而不能保证整体的ID唯一。为了避免这种情况,我们有以下两种方式解决该问题。

    2.1、主键表

    通过单独创建主键表维护唯一标识,作为ID的输出源可以保证整体ID的唯一。举个例子:

    创建一个主键表

    CREATE TABLE `unique_id`  (
      `id` bigint NOT NULL AUTO_INCREMENT,
      `biz` char(1NOT NULL,
      PRIMARY KEY (`id`),
     UNIQUE KEY `biz` (`biz`)
    ENGINE = InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET =utf8;

    业务通过更新操作来获取ID信息,然后添加到某个分表中。

    BEGIN;

    REPLACE INTO unique_id (biz) values ('o') ;
    SELECT LAST_INSERT_ID();

    COMMIT;

    2.2、ID自增步长设置

    我们可以设置Mysql主键自增步长,让分布在不同实例的表数据ID做到不重复,保证整体的唯一。

    如下,可以设置Mysql实例1步长为1,实例1步长为2。

    查看主键自增的属性

    show variables like '%increment%'

    显然,这种方式在并发量比较高的情况下,如何保证扩展性其实会是一个问题。

    3、号段模式

    号段模式是当下分布式ID生成器的主流实现方式之一。其原理如下:

    • 号段模式每次从数据库取出一个号段范围,加载到服务内存中。业务获取时ID直接在这个范围递增取值即可。
    • 等这批号段ID用完,再次向数据库申请新号段,对max_id字段做一次update操作,新的号段范围是(max_id ,max_id +step]。
    • 由于多业务端可能同时操作,所以采用版本号version乐观锁方式更新。

    例如 (1,1000] 代表1000个ID,具体的业务服务将本号段生成1~1000的自增ID。表结构如下:

    CREATE TABLE id_generator (
      id int(10NOT NULL,
      max_id bigint(20NOT NULL COMMENT '当前最大id',
      step int(20NOT NULL COMMENT '号段的长度',
      biz_type    int(20NOT NULL COMMENT '业务类型',
      version int(20NOT NULL COMMENT '版本号,是一个乐观锁,每次都更新version,保证并发时数据的正确性',
      PRIMARY KEY (`id`)

    这种分布式ID生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多。但同样也会存在一些缺点比如:服务器重启,单点故障会造成ID不连续。

    4、Redis INCR

    基于全局唯一ID的特性,我们可以通过Redis的INCR命令来生成全局唯一ID。

    Redis分布式ID的简单案例

    /**
     *  Redis 分布式ID生成器
     */

    @Component
    public class RedisDistributedId {

        @Autowired
        private StringRedisTemplate redisTemplate;

        private static final long BEGIN_TIMESTAMP = 1659312000l;

        /**
         * 生成分布式ID
         * 符号位    时间戳[31位]  自增序号【32位】
         * @param item
         * @return
         */

        public long nextId(String item){
            // 1.生成时间戳
            LocalDateTime now = LocalDateTime.now();
            // 格林威治时间差
            long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
            // 我们需要获取的 时间戳 信息
            long timestamp = nowSecond - BEGIN_TIMESTAMP;
            // 2.生成序号 --》 从Redis中获取
            // 当前当前的日期
            String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
            // 获取对应的自增的序号
            Long increment = redisTemplate.opsForValue().increment("id:" + item + ":" + date);
            return timestamp 32 | increment;
        }

    }

    同样使用Redis也有对应的缺点:ID 生成的持久化问题,如果Redis宕机了怎么进行恢复?

    5、雪花算法

    Snowflake,雪花算法是有Twitter开源的分布式ID生成算法,以划分命名空间的方式将64bit位分割成了多个部分,每个部分都有具体的不同含义,在Java中64Bit位的整数是Long类型,所以在Java中Snowflake算法生成的ID就是long来存储的。具体如下:

    • 第一部分: 占用1bit,第一位为符号位,不适用
    • 第二部分: 41位的时间戳,41bit位可以表示241个数,每个数代表的是毫秒,那么雪花算法的时间年限是(241)/(1000×60×60×24×365)=69
    • 第三部分: 10bit表示是机器数,即 2^ 10 = 1024台机器,通常不会部署这么多机器
    • 第四部分: 12bit位是自增序列,可以表示2^12=4096个数,一秒内可以生成4096个ID,理论上snowflake方案的QPS约为409.6w/s

    雪花算法案例代码:

    public class SnowflakeIdWorker {

        // ==============================Fields===========================================
        /**
         * 开始时间截 (2020-11-03,一旦确定不可更改,否则时间被回调,或者改变,可能会造成id重复或冲突)
         */

        private final long twepoch = 1604374294980L;

        /**
         * 机器id所占的位数
         */

        private final long workerIdBits = 5L;

        /**
         * 数据标识id所占的位数
         */

        private final long datacenterIdBits = 5L;

        /**
         * 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
         */

        private final long maxWorkerId = -1L ^ (-1L 
        /**
         * 支持的最大数据标识id,结果是31
         */

        private final long maxDatacenterId = -1L ^ (-1L 
        /**
         * 序列在id中占的位数
         */

        private final long sequenceBits = 12L;

        /**
         * 机器ID向左移12位
         */

        private final long workerIdShift = sequenceBits;

        /**
         * 数据标识id向左移17位(12+5)
         */

        private final long datacenterIdShift = sequenceBits + workerIdBits;

        /**
         * 时间截向左移22位(5+5+12)
         */

        private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

        /**
         * 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
         */

        private final long sequenceMask = -1L ^ (-1L 
        /**
         * 工作机器ID(0~31)
         */

        private long workerId;

        /**
         * 数据中心ID(0~31)
         */

        private long datacenterId;

        /**
         * 毫秒内序列(0~4095)
         */

        private long sequence = 0L;

        /**
         * 上次生成ID的时间截
         */

        private long lastTimestamp = -1L;

        //==============================Constructors=====================================

        /**
         * 构造函数
         *
         */

        public SnowflakeIdWorker() {
            this.workerId = 0L;
            this.datacenterId = 0L;
        }

        /**
         * 构造函数
         *
         * @param workerId     工作ID (0~31)
         * @param datacenterId 数据中心ID (0~31)
         */

        public SnowflakeIdWorker(long workerId, long datacenterId) {
            if (workerId > maxWorkerId || workerId 0) {
                throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
            }
            if (datacenterId > maxDatacenterId || datacenterId 0) {
                throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
            }
            this.workerId = workerId;
            this.datacenterId = datacenterId;
        }

        // ==============================Methods==========================================

        /**
         * 获得下一个ID (该方法是线程安全的)
         *
         * @return SnowflakeId
         */

        public synchronized long nextId() {
            long timestamp = timeGen();

            //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
            if (timestamp             throw new RuntimeException(
                        String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
            }

            //如果是同一时间生成的,则进行毫秒内序列
            if (lastTimestamp == timestamp) {
                sequence = (sequence + 1) & sequenceMask;
                //毫秒内序列溢出
                if (sequence == 0) {
                    //阻塞到下一个毫秒,获得新的时间戳
                    timestamp = tilNextMillis(lastTimestamp);
                }
            }
            //时间戳改变,毫秒内序列重置
            else {
                sequence = 0L;
            }

            //上次生成ID的时间截
            lastTimestamp = timestamp;

            //移位并通过或运算拼到一起组成64位的ID
            return ((timestamp - twepoch) //
                    | (datacenterId //
                    | (workerId //
                    | sequence;
        }

        /**
         * 阻塞到下一个毫秒,直到获得新的时间戳
         *
         * @param lastTimestamp 上次生成ID的时间截
         * @return 当前时间戳
         */

        protected long tilNextMillis(long lastTimestamp) {
            long timestamp = timeGen();
            while (timestamp             timestamp = timeGen();
            }
            return timestamp;
        }

        /**
         * 返回以毫秒为单位的当前时间
         *
         * @return 当前时间(毫秒)
         */

        protected long timeGen() {
            return System.currentTimeMillis();
        }

        /**
         * 随机id生成,使用雪花算法
         *
         * @return
         */

        public static String getSnowId() {
            SnowflakeIdWorker sf = new SnowflakeIdWorker();
            String id = String.valueOf(sf.nextId());
            return id;
        }

        //=========================================Test=========================================

        /**
         * 测试
         */

        public static void main(String[] args) {
            SnowflakeIdWorker idWorker = new SnowflakeIdWorker(00);
            for (int i = 0; i 1000; i++) {
                long id = idWorker.nextId();
                System.out.println(id);
            }
        }
    }

    雪花算法强依赖机器时钟,如果机器上时钟回拨,会导致发号重复。通常通过记录最后使用时间处理该问题。

    6、美团(Leaf)

    由美团开发,开源项目链接:

    • https://github.com/Meituan-Dianping/Leaf

    Leaf同时支持号段模式和snowflake算法模式,可以切换使用。

    snowflake模式依赖于ZooKeeper,不同于原始snowflake算法也主要是在workId的生成上,Leaf中workId是基于ZooKeeper的顺序Id来生成的,每个应用在使用Leaf-snowflake时,启动时都会都在Zookeeper中生成一个顺序Id,相当于一台机器对应一个顺序节点,也就是一个workId。

    号段模式是对直接用数据库自增ID充当分布式ID的一种优化,减少对数据库的频率操作。相当于从数据库批量的获取自增ID,每次从数据库取出一个号段范围,例如 (1,1000] 代表1000个ID,业务服务将号段在本地生成1~1000的自增ID并加载到内存。

    7、百度(Uidgenerator)

    源码地址:

    • https://github.com/baidu/uid-generator

    中文文档地址:

    • https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md

    UidGenerator是百度开源的Java语言实现,基于Snowflake算法的唯一ID生成器。它是分布式的,并克服了雪花算法的并发限制。单个实例的QPS能超过6000000。需要的环境:JDK8+,MySQL(用于分配WorkerId)。

    百度的Uidgenerator对结构做了部分的调整,具体如下:

    时间部分只有28位,这就意味着UidGenerator默认只能承受8.5年(2^28-1/86400/365),不过UidGenerator可以适当调整delta seconds、worker node id和sequence占用位数。

    8、滴滴(TinyID)

    由滴滴开发,开源项目链接:

    • https://github.com/didi/tinyid

    Tinyid是在美团(Leaf)的leaf-segment算法基础上升级而来,不仅支持了数据库多主节点模式,还提供了tinyid-client客户端的接入方式,使用起来更加方便。但和美团(Leaf)不同的是,Tinyid只支持号段一种模式不支持雪花模式。Tinyid提供了两种调用方式,一种基于Tinyid-server提供的http方式,另一种Tinyid-client客户端方式。

    总结比较

    作者:叫我二蛋

    来源:wangbinguang.blog.csdn.net/article/

    details/129201971


  • ES 不香吗,为啥还要 ClickHouse?

    快乐分享,Java干货及时送达👇

    文章来https://zhuanlan.zhihu.com/p/353296392

    目录
    • 架构和设计的对比
    • 查询对比实战
    • 总结

    前言


    Elasticsearch 是一个实时的分布式搜索分析引擎,它的底层是构建在Lucene之上的。简单来说是通过扩展Lucene的搜索能力,使其具有分布式的功能。ES通常会和其它两个开源组件logstash(日志采集)和Kibana(仪表盘)一起提供端到端的日志/搜索分析的功能,常常被简称为ELK。

    Clickhouse是俄罗斯搜索巨头Yandex开发的面向列式存储的关系型数据库。ClickHouse是过去两年中OLAP领域中最热门的,并于2016年开源。

    ES是最为流行的大数据日志和搜索解决方案,但是近几年来,它的江湖地位受到了一些挑战,许多公司已经开始把自己的日志解决方案从ES迁移到了Clickhouse,这里就包括:携程,快手等公司。


    架构和设计的对比


    ES的底层是Lucenc,主要是要解决搜索的问题。搜索是大数据领域要解决的一个常见的问题,就是在海量的数据量要如何按照条件找到需要的数据。搜索的核心技术是倒排索引和布隆过滤器。ES通过分布式技术,利用分片与副本机制,直接解决了集群下搜索性能与高可用的问题。

    ElasticSearch是为分布式设计的,有很好的扩展性,在一个典型的分布式配置中,每一个节点(node)可以配制成不同的角色,如下图所示:

    • Client Node,负责API和数据的访问的节点,不存储/处理数据
    • Data Node,负责数据的存储和索引
    • Master Node, 管理节点,负责Cluster中的节点的协调,不存储数据。

    ClickHouse是基于MPP架构的分布式ROLAP(关系OLAP)分析引擎。每个节点都有同等的责任,并负责部分数据处理(不共享任何内容)。ClickHouse 是一个真正的列式数据库管理系统(DBMS)。在 ClickHouse 中,数据始终是按列存储的,包括矢量(向量或列块)执行的过程。让查询变得更快,最简单且有效的方法是减少数据扫描范围和数据传输时的大小,而列式存储和数据压缩就可以帮助实现上述两点。Clickhouse同时使用了日志合并树,稀疏索引和CPU功能(如SIMD单指令多数据)充分发挥了硬件优势,可实现高效的计算。Clickhouse 使用Zookeeper进行分布式节点之间的协调。

    为了支持搜索,Clickhouse同样支持布隆过滤器。


    查询对比实战


    为了对比ES和Clickhouse的基本查询能力的差异,我写了一些代码(https://github.com/gangtao/esvsch)来验证。

    这个测试的架构如下:

    架构主要有四个部分组成:

    • ES stack ES stack有一个单节点的Elastic的容器和一个Kibana容器组成,Elastic是被测目标之一,Kibana作为验证和辅助工具。部署代码如下:
    version: '3.7'

    services:
      elasticsearch:
        image: docker.elastic.co/elasticsearch/elasticsearch:7.4.0
        container_name: elasticsearch
        environment:
          - xpack.security.enabled=false
          - discovery.type=single-node
        ulimits:
          memlock:
            soft: -1
            hard: -1
          nofile:
            soft: 65536
            hard: 65536
        cap_add:
          - IPC_LOCK
        volumes:
          - elasticsearch-data:/usr/share/elasticsearch/data
        ports:
          - 9200:9200
          - 9300:9300
        deploy:
          resources:
            limits:
              cpus: '4'
              memory: 4096M
            reservations:
              memory: 4096M

      kibana:
        container_name: kibana
        image: docker.elastic.co/kibana/kibana:7.4.0
        environment:
          - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
        ports:
          - 5601:5601
        depends_on:
          - elasticsearch

    volumes:
      elasticsearch-data:
        driver: local
    • Clickhouse stack Clickhouse stack有一个单节点的Clickhouse服务容器和一个TabixUI作为Clickhouse的客户端。部署代码如下:
    version: "3.7"
    services:
      clickhouse:
        container_name: clickhouse
        image: yandex/clickhouse-server
        volumes:
          - ./data/config:/var/lib/clickhouse
        ports:
          - "8123:8123"
          - "9000:9000"
          - "9009:9009"
          - "9004:9004"
        ulimits:
          nproc: 65535
          nofile:
            soft: 262144
            hard: 262144
        healthcheck:
          test: ["CMD", "wget", "--spider", "-q", "localhost:8123/ping"]
          interval: 30s
          timeout: 5s
          retries: 3
        deploy:
          resources:
            limits:
              cpus: '4'
              memory: 4096M
            reservations:
              memory: 4096M

      tabixui:
        container_name: tabixui
        image: spoonest/clickhouse-tabix-web-client
        environment:
          - CH_NAME=dev
          - CH_HOST=127.0.0.1:8123
          - CH_LOGIN=default
        ports:
          - "18080:80"
        depends_on:
          - clickhouse
        deploy:
          resources:
            limits:
              cpus: '0.1'
              memory: 128M
            reservations:
              memory: 128M
    • 数据导入 stack 数据导入部分使用了Vector.dev开发的vector,该工具和fluentd类似,都可以实现数据管道式的灵活的数据导入。
    • 测试控制 stack 测试控制我使用了Jupyter,使用了ES和Clickhouse的Python SDK来进行查询的测试。

    用Docker compose启动ES和Clickhouse的stack后,我们需要导入数据,我们利用Vector的generator功能,生成syslog,并同时导入ES和Clickhouse,在这之前,我们需要在Clickhouse上创建表。ES的索引没有固定模式,所以不需要事先创建索引。

    创建表的代码如下:

    CREATE TABLE default.syslog(
        application String,
        hostname String,
        message String,
        mid String,
        pid String,
        priority Int16,
        raw String,
        timestamp DateTime('UTC'),
        version Int16
    ENGINE = MergeTree()
        PARTITION BY toYYYYMMDD(timestamp)
        ORDER BY timestamp
        TTL timestamp + toIntervalMonth(1);

    创建好表之后,我们就可以启动vector,向两个stack写入数据了。vector的数据流水线的定义如下:

    [sources.in]
    type = "generator"
    format = "syslog"
    interval = 0.01
    count = 100000

    [transforms.clone_message]
    type = "add_fields"
    inputs = ["in"]
    fields.raw = "{{ message }}"

    [transforms.parser]
    # General
    type = "regex_parser"
    inputs = ["clone_message"]
    field = "message" # optional, default
    patterns = ['^d*)>(?Pd) (?Pd{4}-d{2}-d{2}Td{2}:d{2}:d{2}.d{3}Z) (?Pw+.w+) (?Pw+) (?Pd+) (?PIDd+) - (?P.*)$']

    [transforms.coercer]
    type = "coercer"
    inputs = ["parser"]
    types.timestamp = "timestamp"
    types.version = "int"
    types.priority = "int"

    [sinks.out_console]
    # General
    type = "console"
    inputs = ["coercer"]
    target = "stdout"

    # Encoding
    encoding.codec = "json"


    [sinks.out_clickhouse]
    host = "http://host.docker.internal:8123"
    inputs = ["coercer"]
    table = "syslog"
    type = "clickhouse"

    encoding.only_fields = ["application", "hostname", "message", "mid", "pid", "priority", "raw", "timestamp", "version"]
    encoding.timestamp_format = "unix"

    [sinks.out_es]
    # General
    type = "elasticsearch"
    inputs = ["coercer"]
    compression = "none"
    endpoint = "http://host.docker.internal:9200"
    index = "syslog-%F"

    # Encoding

    # Healthcheck
    healthcheck.enabled = true

    这里简单介绍一下这个流水线:

    • http://source.in 生成syslog的模拟数据,生成10w条,生成间隔和0.01秒
    • transforms.clone_message 把原始消息复制一份,这样抽取的信息同时可以保留原始消息
    • transforms.parser 使用正则表达式,按照syslog的定义,抽取出application,hostname,message ,mid ,pid ,priority ,timestamp ,version 这几个字段
    • transforms.coercer 数据类型转化
    • sinks.out_console 把生成的数据打印到控制台,供开发调试
    • sinks.out_clickhouse 把生成的数据发送到Clickhouse
    • sinks.out_es 把生成的数据发送到ES

    运行Docker命令,执行该流水线:

    docker run 
      -v $(mkfile_path)/vector.toml:/etc/vector/vector.toml:ro 
      -p 18383:8383 
      timberio/vector:nightly-alpine

    数据导入后,我们针对一下的查询来做一个对比。ES使用自己的查询语言来进行查询,Clickhouse支持SQL,我简单测试了一些常见的查询,并对它们的功能和性能做一些比较。

    • 返回所有的记录
    # ES
    {
      "query":{
        "match_all":{}
      }
    }

    # Clickhouse
    "SELECT * FROM syslog"
    • 匹配单个字段
    # ES
    {
      "query":{
        "match":{
          "hostname":"for.org"
        }
      }
    }

    # Clickhouse
    "SELECT * FROM syslog WHERE hostname='for.org'"
    • 匹配多个字段
    # ES
    {
      "query":{
        "multi_match":{
          "query":"up.com ahmadajmi",
            "fields":[
              "hostname",
              "application"
            ]
        }
      }
    }

    # Clickhouse、
    "SELECT * FROM syslog WHERE hostname='for.org' OR application='ahmadajmi'"
    • 单词查找,查找包含特定单词的字段
    # ES
    {
      "query":{
        "term":{
          "message":"pretty"
        }
      }
    }

    # Clickhouse
    "SELECT * FROM syslog WHERE lowerUTF8(raw) LIKE '%pretty%'"
    • 范围查询, 查找版本大于2的记录
    # ES
    {
      "query":{
        "range":{
          "version":{
            "gte":2
          }
        }
      }
    }

    # Clickhouse
    "SELECT * FROM syslog WHERE version >= 2"
    • 查找到存在某字段的记录
      ES是文档类型的数据库,每一个文档的模式不固定,所以会存在某字段不存在的情况;而Clickhouse对应为字段为空值
    # ES
    {
      "query":{
        "exists":{
          "field":"application"
        }
      }
    }

    # Clickhouse
    "SELECT * FROM syslog WHERE application is not NULL"
    • 正则表达式查询,查询匹配某个正则表达式的数据
    # ES
    {
      "query":{
        "regexp":{
          "hostname":{
            "value":"up.*",
              "flags":"ALL",
                "max_determinized_states":10000,
                  "rewrite":"constant_score"
          }
        }
      }
    }

    # Clickhouse
    "SELECT * FROM syslog WHERE match(hostname, 'up.*')"
    • 聚合计数,统计某个字段出现的次数
    # ES
    {
      "aggs":{
        "version_count":{
          "value_count":{
            "field":"version"
          }
        }
      }
    }

    # Clickhouse
    "SELECT count(version) FROM syslog"
    • 聚合不重复的值,查找所有不重复的字段的个数
    # ES
    {
      "aggs":{
        "my-agg-name":{
          "cardinality":{
            "field":"priority"
          }
        }
      }
    }

    # Clickhouse
    "SELECT count(distinct(priority)) FROM syslog "

    我用Python的SDK,对上述的查询在两个Stack上各跑10次,然后统计查询的性能结果。

    我们画出出所有的查询的响应时间的分布:

    总查询时间的对比如下:

    通过测试数据我们可以看出Clickhouse在大部分的查询的性能上都明显要优于Elastic。在正则查询(Regex query)和单词查询(Term query)等搜索常见的场景下,也并不逊色。

    在聚合场景下,Clickhouse表现异常优秀,充分发挥了列村引擎的优势。

    注意,我的测试并没有任何优化,对于Clickhouse也没有打开布隆过滤器。可见Clickhouse确实是一款非常优秀的数据库,可以用于某些搜索的场景。当然ES还支持非常丰富的查询功能,这里只有一些非常基本的查询,有些查询可能存在无法用SQL表达的情况。


    总结


    本文通过对于一些基本查询的测试,对比了Clickhouse 和Elasticsearch的功能和性能,测试结果表明,Clickhouse在这些基本场景表现非常优秀,性能优于ES,这也解释了为什么用很多的公司应从ES切换到Clickhouse之上。

  • 首次力压 macOS!这次 Linux 杀疯了!!

    快乐分享,Java干货及时送达👇

    文章来:【公众号:量子位】


    2022年是Linux桌面版之年。

    一位来自亚马逊K8s团队的程序员在自己最新的博客上这样写道。

    何出此言?

    原来是根据Stack Overflow 2022年开发者调查结果得出。

    该报告显示,2022年将Linux作为主要操作系统的比例已经达到了40.23%,不仅超过了macOS,还将差距拉到了9%

    要知道,去年这俩还基本持平,差距仅为0.13%。

    而且,这还不算15%的用户选择WSL的情况,即在Windows系统上运行Linux子系统。

    打出生时就为服务器而生的Linux,真的这么火了?

    首次力压macOS

    Stack Overflow今年这份调查一共有7万多人参与。

    操作系统方面,主要分为“个人使用”和“工作使用”,调查大家在这两种情况下最常用的操作系统。

    结果是无论哪种情况,Linux系统都超过了macOS,尤其以个人使用为甚。

    具体来说,在接收到的71503份结果中,有28765位调查者在个人使用方面选择了Linux系统,占比为40.23%;

    有22217位选择了macOS,占比为31.07%。两者差距近10%。

    而在工作使用方面,选择Linux系统的达到了28523位,占比39.89%,和个人使用基本持平;

    选择macOS的则有23578位,占比32.97%,比个人使用要多一些(这是macOS最特别的地方)。但它和Linux的差距仍达到了近7%。

    除此之外,还有15%左右的人无论是在个人使用还是工作场景都会选择微软的WSL(Windows Subsystem for Linux),进一步证明Linux的受欢迎程度。

    而从往年数据来看,Linux的受欢迎程度一直小步攀升,今年是首次与macOS的差距拉开这么多。

    所以,难怪开头的程序员管今年叫“Linux桌面版之年”。

    具体来看,2018-2020年之间,Linux的数据分别为23.2%、25.6%、 26.6%,一直屈居第三位。

    2021年是分水岭,Linux首次以0.13%的微妙差距超过macOS,成为第二名。

    不过在工作场景中,macOS还是更胜一筹(30.04%VS25.17%)

    到了2022年,Linux一下子就在个人和工作两方面都大比分超过了macOS。

    如Stack Overflow官方所说,这证明了开源软件的吸引力

    当然,它和Windows系统的差距还是不少,后者仍然是三大操作系统里的王者。

    而除了操作系统本身,其他调查的数据也显示,Linux在Steam平台的市场份额近来也一直在提升。

    2022年1月,该平台上Linux玩家占比1.06%,而到了11月,这个数字涨到了1.44%,而这主要归功于Steam Deck这款掌机的上市(Windows仍然是统治地位的96.11%)

    就在2022年10月的Akademy 2022会议上,相关人员透露,Steam Deck的出货量已超过100万个,同时还有一大批延期订单在处理。

    Linux真的这么火了吗?

    还是有网友对如上数据提出了质疑。

    这主要是因为Stack Overflow这个调查中,几大操作系统的数据总和加起来不再等于100%

    TA表示,这个结果说明在选择“您最主要的操作系统时”,很多人都不止选了一个。

    这个数据对于主要只将它用于工作/专业场景的人来说,高得令人难以置信;对于经常在日常也使用Linux的开发人员来说,又低得要命。

    很多人仍然不习惯Linux,他们吐槽的理由包括不太友好的用户UI(即使Ubuntu也让他们受不了)、安装麻烦、包管理复杂等等。

    不过,还是有不少人认为Linux确实越来越火了。

    一位网友表示,Linux的数据或许还会再高一些,毕竟有用户可能本身使用Windows或Mac桌面,但却主要通过远程终端或虚拟机在Linux系统上工作。

    另一位网友则称自己在过去五年里,亲身经历Linux在他们的工作环境中从“很奇怪”、“不常见”变成“再正常不过的事儿”

    甚至有几个非技术岗位的朋友也开始考虑是否要在Thinkpad上运行Linux。

    在TA看来,Linux兴起的因素有很多,包括云的兴起、Linux桌面发行版的成熟、Linux是树莓派等产品的默认/唯一选项、开发者软件越来越支持多平台,以及特别是Linux的硬件兼容性越来越好(以Manjaro版本为甚)等。

    当然,还有人就是喜欢Linux的无广告,和定制化的能力。

    转移到Linux系统的人还有很多,比如这位:

    不仅自己基本放弃Mac,还希望自己公司的员工都转移到Linux上。

    只不过,TA称唯一的阻碍因素是还没有为Linux硬件和软件找到一个好的MDM(移动设备管理)解决方案。

    最后有意思的是,有人既无法抵抗Linux的吸引力,也无法放下macOS,于是“私人用Linux,工作用macOS就成了一个很好的妥协”。

    你最常用什么系统?为什么?

    One More Thing

    最后,再来看看2022年的Stack Overflow开发者调查报告还有哪些亮点。

    1、编程语言方面,Rust已连续第七年成为最受喜爱的语言,约87%的开发人员表示他们希望继续使用它。

    同时,它与Python、TypeScript一起成为最想学习的前三大新语言。

    2、2021年,Git还是大家最常用的基础工具,完全碾压其后的Docker、Yarn等。今年Docker已取代Git夺得第一,使用率从55%增长到69%。

    此外,本项调查还显示,相比专业开发人员,正在学习编码的人更有可能使用3D工具来自学3D VR和AR技术:Unity 3D(23%VS8%)和Unreal Engine(9%VS3%)

    3、Docker和Kubernetes分别位列最受喜爱和想要学习的工具第一和第二位。随着Docker的数据从去年的30%增加到今年的37%,可以看出大家想要使用Docker的愿望并没有放缓。


    4、Phoenix取代Svelte成为最受欢迎的Web框架。Angular.js连续三年成为开发者最讨厌的框架,React.js连续五年成为开发者最想学习的框架。

    5、收入最高的语言仍然是Clojure。

    工具方面,Chef开发人员薪水最高,但它也是开发者最恐怖的工具之一。

    数据库系统方面,收入最高的前三是DynamoDB、Couchbase和Cassandra。

    6、喜欢在线学习编程的人数从60%上升到了70%,相比年轻人(18岁以下),45岁以上的受访者喜欢从书本上学习。

    7、62%的受访者每天花费超过30分钟解决问题;25%的人每天花费一个多小时。

    对于一个由50名开发人员组成的团队来说,每周花费在搜索答案/解决方案上的时间总计333-651小时。

    8、85%的开发人员表示,他们的公司支持远程办公。

    完整报告:
    https://survey.stackoverflow.co/2022/#section-most-popular-technologies-operating-system

    参考链接:
    [1]
    https://www.justingarrison.com/blog/year-of-linux-desktop/
    [2]https://survey.stackoverflow.co/2022/#section-most-popular-technologies-operating-system

  • 船新 IDEA 2023.1 正式发布,新特性真香!

    大家好,昨晚看到 IDEA 官推宣布 IntelliJ IDEA 2023.1 正式发布了。简单看了一下,发现这次的新版本包含了许多改进,进一步优化了用户体验,提高了便捷性。

    至于是否升级最新版本完全是个人意愿,如果觉得新版本没有让自己感兴趣的改进,完全就不用升级,影响不大。软件的版本迭代非常正常,正确看待即可,不持续改进就会慢慢被淘汰!

    根据官方介绍:

    IntelliJ IDEA 2023.1 针对新的用户界面进行了大量重构,这些改进都是基于收到的宝贵反馈而实现的。官方还实施了性能增强措施,使得 Maven 导入更快,并且在打开项目时 IDE 功能更早地可用。由于后台提交检查,新版本提供了简化的提交流程。IntelliJ IDEA Ultimate 现在支持 Spring Security 匹配器和请求映射导航。

    下面对这个版本的一些比较有意思的改进进行详细介绍。

    新 UI 增强(测试版)

    针对收到的有关 IDE 新用户界面的反馈,IntelliJ IDEA 官方实施了一些更新,以解决最受欢迎的请求。引入了紧凑模式,通过缩小间距和元素提供更加集中的 IDE 外观和感觉。新 UI 现在提供一个选项来垂直分割工具窗口区域,并方便地排列窗口,就像旧 UI 一样。主窗口标题栏中的运行小部件已经重新设计,使其外观不显眼且更易于查看。

    在项目打开时更早提供 IDE 功能

    IntelliJ IDEA 官方通过在智能模式下执行扫描文件以建立索引的过程来改进了 IDE 启动体验,这样即可使 IDE 的全部功能在启动过程中更早地可用。当打开一个项目时,IntelliJ IDEA 2023.1 会使用上一次与该项目的会话中存在的缓存,并同时查找要建立索引的文件。如果扫描中没有发现任何更改,则 IDE 将准备就绪,消除了之前由于启动时进行索引而导致的延迟。

    更快地导入 Maven 项目

    更快地导入 Maven 项目

    官方通过优化依赖解析以及重新设计导入和配置 facets 的过程,显著提高了 IDE 在导入 Maven 项目时的性能。

    后台提交检查

    后台提交检查

    官方重新设计了 Git 和 Mercurial 的提交检查行为,以加速整个提交过程。现在,在提交但尚未推送之前会在后台执行检查。

    Spring Security 匹配器和请求映射的导航

    Spring Security 匹配器和请求映射的导航

    为了简化查看应用安全规则,IntelliJ IDEA Ultimate 2023.1 提供了从 Spring 控制器到安全匹配器的轻松导航。该导航可以从安全匹配器到控制器以及反向工作。

    全 IDE 缩放

    全 IDE 缩放

    在 v2023.1 中,可以完全放大和缩小 IDE,同时增加或缩减所有 UI 元素的大小。从主菜单中,选择 View | Appearance(视图 | 外观),调整 IDE 的缩放比例。此外,您可以在 Settings/Preferences | Keymap | Main Menu | View | Appearance(设置/偏好设置 | 按键映射 | 主菜单 | 视图 | 外观)中指定调用这些操作的自定义快捷键。

    新的 Java 检查

    新的 Java 检查

    官方为了帮助保持代码整洁和无错误,升级了一些现有的 Java 检查,并添加了新的检查。格式不正确字符串检查现在报告不符合常见 Java 语法的非法时间转换。冗余字符串操作检查现在能够检测到多余的 StringBuilder.toString() 调用,并提供一个快速修复来将它们替换为 contentEquals(),以便您不会创建中间 String 对象。它还报告 String 构造函数调用中不必要的参数,并建议一个快速修复来删除它们。在这篇博客文章中了解更多关于 IntelliJ IDEA 2023.1 其他代码检查改进。

    Java 20 支持

    Java 20 支持

    继续减少 Java 开发人员认知负荷,IntelliJ IDEA 2023.1 支持最新更新添加到 Java 20 中,包括语言特性模式匹配和记录模式的更改。

    改进了 Extract Method(提取方法)重构

    改进了 Extract Method(提取方法)重构

    官方通过引入选项来升级提取方法重构,即使所选代码片段具有需要返回的多个变量也可以应用该选项。在这些情况下,IDE 首先建议将这些变量封装到一个新记录或 bean 类中,然后执行方法提取。

    VM Options(虚拟机选项)字段中的自动补全

    VM Options(虚拟机选项)字段中的自动补全

    自动补全功能以及集成到 Run/Debug configuration(运行/调试配置)弹出窗口的 VM Options(虚拟机选项)字段中。现在,输入标志的名称时,IDE 会建议可用命令行选项的列表。这适用于 -XX:-X 选项,以及一些未由 IntelliJ IDEA 自动配置的标准选项,如 -ea,但不适用于 -cp–release

    Spring Security 6 支持

    Spring Security 6 支持

    IntelliJ IDEA Ultimate 2023.1 提供了更新的支持,可以导航到 Spring Security 6 中引入的 API 的 URL 映射和安全角色。

    Apache Dubbo 支持

    IntelliJ IDEA 实现了一个新的专用插件,集成了 Apache Dubbo,将该框架的功能作为 IntelliJ IDEA 对 Spring 的支持的一部分。

    Structure(结构)工具窗口中的 VCS 状态颜色提示

    Structure(结构)工具窗口中的 VCS 状态颜色提示

    针对 GitHub 改进了代码审查工作流

    针对 GitHub 改进了代码审查工作流

    为了简化在 IDE 中审查代码的过程,重做了 Pull Request(拉取请求)工具窗口。它现在为您打开的每个拉取请求提供一个专用标签页。标签页会立即显示已更改文件的列表,但它提供的信息比先前更少,让您可以更好地专注于当前任务。现在,可以通过一个新增的专属按钮轻松执行拉取请求当前状态下最相关的操作。

    参考资料

    IntelliJ IDEA 2023.1 更多改进的介绍请参考官方文档:https://www.jetbrains.com/zh-cn/idea/whatsnew/

    
    

  • Java8 Lambda 表达式中的 forEach 如何提前终止?

    快乐分享,Java干货及时送达👇

    # 情景展示

    图片

    如上图所示,我们想要终止for循环,使用return。

    执行结果如下:

    图片

    我们可以看到,只有赵六没被打印出来,后续的数组元素依旧被执行了。

    也就是说,关键字”return”,在这里执行的效果相当于普通for循环里的关键词continue”。

    # 原因分析

    我们知道,在普通for循环里面,想要提前结束(终止)循环体使用”break”;

    结束本轮循环,进行下一轮循环使用”continue”;

    另外,在普通for里,如果使用”return”,不仅强制结束for循环体,还会提前结束包含这个循环体的整个方法。

    而在Java8中的forEach()中,”break”或”continue”是不被允许使用的,而return的意思也不是原来return代表的含义了。

    我们来看看源码:

    图片

    forEach(),说到底是一个方法,而不是循环体,结束一个方法的执行用什么?当然是return啦;

    java8的forEach()和JavaScript的forEach()用法是何其的相似

    Java不是万能的,不要再吐槽它垃圾了。

    # 解决方案

    方案一:使用原始的foreach循环

    图片

    使用过eclipse的老铁们应该知道,当我们输入:foreach,再按快捷键:Alt+/,就会出现foreach的代码提示。

    如上图所示,这种格式的for循环才是真正意义上的foreach循环。

    在idea中输入,按照上述操作是不会有任何代码提示的,那如何才能在idea中,调出来呢?

    图片

    for循环可以提前终止。

    方式一:break

    图片

    方式二:return(不推荐使用)

    图片

    方案二:抛出异常

    我们知道,要想结束一个方法的执行,正常的逻辑是:使用return;

    但是,在实际运行中,往往有很多不突发情况导致代码提前终止,比如:空指针异常,其实,我们也可以通过抛出假异常的方式来达到终止forEach()方法的目的。

    图片

    如果觉得这种方式不友好,可以再包装一层。

    图片

    这样,就完美了。

    这里,需要注意的一点是:要确保你forEach()方法体内不能有其它代码可能会抛出的异常与自己手动抛出并捕获的异常一样;

    否则,当真正该因异常导致代码终止的时候,因为咱们手动捕获了并且没做任何处理,岂不是搬起石头砸自己的脚吗?

    来源 | https://blog.csdn.net/weixin_39597399/article/details/114232746

    
    

  • 面试官:一台服务器最大能支持多少条 TCP 连接?问倒一大片。。。

    快乐分享,Java干货及时送达👇

    来源:juejin.cn/post/7162824884597293086

    • 一台服务器最大能打开的文件数
    • 调整服务器能打开的最大文件数示例
    • 一台服务器最大能支持多少连接
    • 一台客户端机器最多能发起多少条连接
    • 其他
    • 相关实际问题
    图片

    之前有一位读者诉苦,有次面试,好不容易(今年行情大家都懂的)熬到到技术终面,谁知道面试官突然放个大招问他:一台服务器最大能支持多少条 TCP 连接,把他直接给问懵逼了 。。。。(请自行脑补那尴尬的场面与气氛)。

    所以,今天就来讨论一下这个问题。

    一台服务器最大能打开的文件数

    限制参数

    我们知道在Linux中一切皆文件,那么一台服务器最大能打开多少个文件呢?Linux上能打开的最大文件数量受三个参数影响,分别是:

    • fs.file-max (系统级别参数) :该参数描述了整个系统可以打开的最大文件数量。但是root用户不会受该参数限制(比如:现在整个系统打开的文件描述符数量已达到fs.file-max ,此时root用户仍然可以使用ps、kill等命令或打开其他文件描述符)。
    • soft nofile(进程级别参数) :限制单个进程上可以打开的最大文件数。只能在Linux上配置一次,不能针对不同用户配置不同的值。
    • fs.nr_open(进程级别参数) :限制单个进程上可以打开的最大文件数。可以针对不同用户配置不同的值。

    这三个参数之间还有耦合关系,所以配置值的时候还需要注意以下三点:

    1. 如果想加大soft nofile,那么hard nofile参数值也需要一起调整。如果因为hard nofile参数值设置的低,那么soft nofile参数的值设置的再高也没有用,实际生效的值会按照二者最低的来。
    2. 如果增大了hard nofile,那么fs.nr_open也都需要跟着一起调整(fs.nr_open参数值一定要大于hard nofile参数值)。如果不小心把hard nofile的值设置的比fs.nr_open还大,那么后果比较严重。会导致该用户无法登录,如果设置的是*,那么所有用户都无法登录。
    3. 如果加大了fs.nr_open,但是是用的echo “xxx” > ../fs/nr_open命令来修改的fs.nr_open的值,那么刚改完可能不会有问题,但是只要机器一重启,那么之前通过echo命令设置的fs.nr_open值便会失效,用户还是无法登录。所以非常不建议使用echo的方式修改内核参数!!!

    调整服务器能打开的最大文件数示例

    假设想让进程可以打开100万个文件描述符,这里用修改conf文件的方式给出一个建议。如果日后工作里有类似的需求可以作为参考。

    vim /etc/sysctl.conf

    fs.file-max=1100000 // 系统级别设置成110万,多留点buffer  
    fs.nr_open=1100000 // 进程级别也设置成110万,因为要保证比 hard nofile大

    使上面的配置生效sysctl -p

    vim /etc/security/limits.conf
        
    // 用户进程级别都设置成100完  
    soft nofile 1000000  
    hard nofile 1000000

    一台服务器最大能支持多少连接

    我们知道TCP连接,从根本上看其实就是client和server端在内存中维护的一组【socket内核对象】(这里也对应着TCP四元组:源IP、源端口、目标IP、目标端口),他们只要能够找到对方,那么就算是一条连接。那么一台服务器最大能建立多少条连接呢?

    • 由于TCP连接本质上可以理解为是client-server端的一对socket内核对象,那么从理论上将应该是【2^32 (ip数) * 2^16 (端口数)】条连接(约等于两百多万亿)。
    • 但是实际上由于受其他软硬件的影响,我们一台服务器不可能能建立这么多连接(主要是受CPU和内存限制)。

    如果只以ESTABLISH状态的连接来算(这些连接只是建立,但是不收发数据也不处理相关的业务逻辑)那么一台服务器最大能建立多少连接呢?以一台4GB内存的服务器为例!

    • 这种情况下,那么能建立的连接数量主要取决于【内存的大小】(因为如果是)ESTABLISH状态的空闲连接,不会消耗CPU(虽然有TCP保活包传输,但这个影响非常小,可以忽略不计)。
    • 我们知道一条ESTABLISH状态的连接大约消耗【3.3KB内存】,那么通过计算得知一台4GB内存的服务器,【可以建立100w+的TCP连接】(当然这里只是计算所有的连接都只建立连接但不发送和处理数据的情况,如果真实场景中有数据往来和处理(数据接收和发送都需要申请内存,数据处理便需要CPU),那便会消耗更高的内存以及占用更多的CPU,并发不可能达到100w+)。

    上面讨论的都是进建立连接的理想情况,在现实中如果有频繁的数据收发和处理(比如:压缩、加密等),那么一台服务器能支撑1000连接都算好的了,所以一台服务器能支撑多少连接还要结合具体的场景去分析,不能光靠理论值去算。抛开业务逻辑单纯的谈并发没有太大的实际意义。

    服务器的开销大头往往并不是连接本身,而是每条连接上的数据收发,以及请求业务逻辑处理!!!

    一台客户端机器最多能发起多少条连接

    我们知道客户端每和服务端建立一个连接便会消耗掉client端一个端口。一台机器的端口范围是【0 ~ 65535】,那么是不是说一台client机器最多和一台服务端机器建立65535个连接呢(这65535个端口里还有很多保留端口,可用端口可能只有64000个左右)?

    由TCP连接的四元组特性可知,只要四元组里某一个元素不同,那么就认为这是不同的TCP连接。所以需要分情况讨论:

    情况一 】、如果一台client仅有一个IP,server端也仅有一个IP并且仅启动一个程序,监听一个端口的情况下,client端和这台server端最大可建立的连接条数就是 65535 个。

    因为源IP固定,目标IP和端口固定,四元组中唯一可变化的就是【源端口】,【源端口】的可用范围又是【0 ~ 65535】,所以一台client机器最大能建立65535个连接。

    情况二 】、如果一台client有多个IP(假设客户端有 n 个IP),server端仅有一个IP并且仅启动一个程序,监听一个端口的情况下,一台client机器最大能建立的连接条数是:n * 65535 个。

    因为目标IP和端口固定,有 n 个源IP,四元组中可变化的就是【源端口】+ 【源IP】,【源端口】的可用范围又是【0 ~ 65535】,所以一个IP最大能建立65535个连接,那么n个IP最大就能建立 n * 65535个连接了。

    以现在的技术,给一个client分配多个IP是非常容易的事情,只需要去联系你们网管就可以做到。

    情况三 】、如果一台client仅有一个IP,server端也仅有一个IP但是server端启动多个程序,每个程序监听一个端口的情况下(比如server端启动了m个程序,监听了m个不同端口),一台client机器最大能建立的连接数量为:65535 * m。

    源IP固定,目标IP固定,目标端口数量为m个,可变化的是源端口,而源端口变化范围是【0 ~ 65535】,所以一台client机器最大能建立的TCP连接数量是 65535 * m个。

    • 其余情况类推,但是客户端的可用端口范围一般达不到65535个,受内核参数net.ipv4.ip_local_port_range限制,如果要修改client所能使用的端口范围,可以修改这个内核参数的值。
    • 所以,不光是一台server端可以接收100w+个TCP连接,一台client照样能发出100w+个连接。

    其他

    • 三次握手里socket的全连接队列长度由参数net.core.somaxconn来控制,默认大小是128,当两台机器离的非常近,但是建立连接的并发又非常高时,可能会导致半连接队列或全连接队列溢出,进而导致server端丢弃握手包。然后造成client超时重传握手包(至少1s以后才会重传),导致三次握手连接建立耗时过长。我们可以调整参数net.core.somaxconn来增加去按连接队列的长度,进而减小丢包的影响
    • 有时候我们通过 ctrl + c方式来终止了某个进程,但是当重启该进程的时候发现报错端口被占用,这种问题是因为【操作系统还没有来得及回收该端口,等一会儿重启应用就好了】
    • client程序在和server端建立连接时,如果client没有调用bind方法传入指定的端口,那么client在和server端建立连接的时候便会自己随机选择一个端口来建立连接。一旦我们client程序调用了bind方法传入了指定的端口,那么client将会使用我们bind里指定的端口来和server建立连接。所以不建议client调用bind方法,bind函数会改变内核选择端口的策略
    public static void main(String[] args) throws IOException {  
        SocketChannel sc = SocketChannel.open();  
       // 客户端还可以调用bind方法  
        sc.bind(new InetSocketAddress("localhost", 9999));  
        sc.connect(new InetSocketAddress("localhost", 8080));  
        System.out.println("waiting..........");  
    }
    • 在Linux一切皆文件,当然也包括之前TCP连接中说的socket。进程打开一个socket的时候需要创建好几个内核对象,换一句直白的话说就是打开文件对象吃内存,所以Linux系统基于安全角度考虑(比如:有用户进程恶意的打开无数的文件描述符,那不得把系统搞奔溃了),在多个位置都限制了可打开的文件描述符的数量。
    • 内核是通过【hash表】的方式来管理所有已经建立好连接的socket,以便于有请求到达时快速的通过【TCP四元组】查找到内核中对应的socket对象。

    在epoll模型中,通过红黑树来管理epoll对象所管理的所有socket,用红黑树结构来平衡快速删除、插入、查找socket的效率。

    相关实际问题

    在网络开发中,很多人对一个基础问题始终没有彻底搞明白,那就是一台机器最多能支撑多少条TCP连接。不过由于客户端和服务端对端口使用方式不同,这个问题拆开来理解要容易一些。

    注意,这里说的是客户端和服务端都只是角色,并不是指某一台具体的机器。例如对于我们自己开发的应用程序来说,当他响应客户端请求的时候,他就是服务端。当他向MySQL请求数据的时候,他又变成了客户端。

    “too many open files” 报错是怎么回事,该如何解决

    你在线上可能遇到过too many open files这个错误,那么你理解这个报错发生的原理吗?如果让你修复这个错误,应该如何处理呢?

    • 因为每打开一个文件(包括socket),都需要消耗一定的内存资源。为了避免个别进程不受控制的打开了过多文件而让整个服务器奔溃,Linux对打开的文件描述符数量有限制。如果你的进程触发到内核的限制,那么”too many open files” 报错就产生了。
    • 可以通过修改fs.file-max 、soft nofile、fs.nr_open这三个参数的值来修改进程能打开的最大文件描述符数量。

    需要注意这三个参数之间的耦合关系!

    一台服务端机器最大究竟能支持多少条连接

    因为这里要考虑的是最大数,因此先不考虑连接上的数据收发和处理,仅考虑ESTABLISH状态的空连接。那么一台服务端机器上最大可以支持多少条TCP连接?这个连接数会受哪些因素的影响?

    • 在不考虑连接上数据的收发和处理的情况下,仅考虑ESTABLISH状态下的空连接情况下,一台服务器上最大可支持的TCP连接数量基本上可以说是由内存大小来决定的。
    • 四元组唯一确定一条连接,但服务端可以接收来自任意客户端的请求,所以根据这个理论计算出来的数字太大,没有实际意义。另外文件描述符限制其实也是内核为了防止某些应用程序不受限制的打开【文件句柄】而添加的限制。这个限制只要修改几个内核参数就可以加大。
    • 一个socket大约消耗3kb左右的内存,这样真正制约服务端机器最大并发数的就是内存,拿一台4GB内存的服务器来说,可以支持的TCP连接数量大约是100w+。
    一条客户端机器最大究竟能支持多少条连接

    和服务端不同的是,客户端每次建立一条连接都需要消耗一个端口。在TCP协议中,端口是一个2字节的整数,因此范围只能是0~65535。那么客户单最大只能支持65535条连接吗?有没有办法突破这个限制,有的话有哪些办法?

    • 客户度每次建立一条连接都需要消耗一个端口。从数字上来看,似乎最多只能建立65535条连接。但实际上我们有两种办法破除65535这个限制。

    方式一,为客户端配置多IP 方式二,分别连接不同的服务端

    • 所以一台client发起百万条连接是没有任何问题的。
    做一个长连接推送产品,支持1亿用户需要多少台机器

    假设你是系统架构师,现在老板给你一个需求,让你做一个类似友盟upush这样的产品。要在服务端机器上保持一个和客户端的长连接,绝大部分情况下连接都是空闲的,每天也就顶多推送两三次左右。总用户规模预计是1亿。那么现在请你来评估一下需要多少台服务器可以支撑这1亿条长连接。

    • 对于长连接推送模块这种服务来说,给客户端发送数据只是偶尔的,一般一天也就顶多一两次。绝大部分情况下TCP连接都是空闲的,CPU开销可以忽略。
    • 再基于内存来考虑,假设服务器内存是128G的,那么一台服务器可以考虑支持500w条并发。这样会消耗掉大约不到20GB内存用来保存这500w条连接对应的socket。还剩下100GB以上的内存来应对接收、发送缓冲区等其他的开销足够了。所以,一亿用户,仅仅需要20台服务器就差不多够用了!


  • Spring Cloud 的25连环炮!

    快乐分享,Java干货及时送达👇

    来源:Java后端面试官

    • 前言
    • Spring Cloud核心知识总结
    • 连环炮走起
    • 总结

    前言

    上周,一位朋友在面试被问到了Spring Cloud,然后结合他的反馈,今天我们继续走起SpringCloud面试连环炮。

    Spring Cloud核心知识总结

    下面是一张Spring Cloud核心组件关系图:

    图片

    从这张图中,其实我们是可以获取很多信息的,希望大家细细品尝。

    话不多说,我们直接开始 Spring Cloud 连环炮。

    连环炮走起

    1、什么是Spring Cloud ?

    Spring cloud 流应用程序启动器是基于 Spring Boot 的 Spring 集成应用程序,提供与外部系统的集成。Spring cloud Task,一个生命周期短暂的微服务框架,用于快速构建执行有限数据处理的应用程序。

    2、什么是微服务?

    微服务架构是一种架构模式或者说是一种架构风格,它提倡将单一应用程序划分为一组小的服务,每个服务运行在其独立的自己的进程中,服务之间相互协调、互相配合,为用户提供最终价值。服务之间采用轻量级的通信机制互相沟通(通常是基于HTTP的RESTful API),每个服务都围绕着具体的业务进行构建,并且能够被独立的构建在生产环境、类生产环境等。另外,应避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具对其进行构建,可以有一个非常轻量级的集中式管理来协调这些服务,可以使用不同的语言来编写服务,也可以使用不同的数据存储。

    通俗地来讲:

    微服务就是一个独立的职责单一的服务应用程序。在 intellij idea 工具里面就是用maven开发的一个个独立的module,具体就是使用springboot 开发的一个小的模块,处理单一专业的业务逻辑,一个模块只做一个事情。

    微服务强调的是服务大小,关注的是某一个点,具体解决某一个问题/落地对应的一个服务应用,可以看做是idea 里面一个 module。

    3、Spring Cloud有什么优势

    使用 Spring Boot 开发分布式微服务时,我们面临以下问题

    • 与分布式系统相关的复杂性-这种开销包括网络问题,延迟开销,带宽问题,安全问题。
    • 服务发现-服务发现工具管理群集中的流程和服务如何查找和互相交谈。它涉及一个服务目录,在该目录中注册服务,然后能够查找并连接到该目录中的服务。
    • 冗余-分布式系统中的冗余问题。
    • 负载平衡 –负载平衡改善跨多个计算资源的工作负荷,诸如计算机,计算机集群,网络链路,中央处理单元,或磁盘驱动器的分布。
    • 性能-问题 由于各种运营开销导致的性能问题。
    • 部署复杂性-Devops 技能的要求。

    4、微服务之间如何独立通讯的?

    同步通信:dobbo通过 RPC 远程过程调用、springcloud通过 REST  接口json调用等。

    异步:消息队列,如:RabbitMqActiveMKafka等消息队列。

    5、什么是服务熔断?什么是服务降级?

    熔断机制是应对雪崩效应的一种微服务链路保护机制。当某个微服务不可用或者响应时间太长时,会进行服务降级,进而熔断该节点微服务的调用,快速返回“错误”的响应信息。当检测到该节点微服务调用响应正常后恢复调用链路。在Spring Cloud框架里熔断机制通过Hystrix实现,Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内调用20次,如果失败,就会启动熔断机制。

    服务降级,一般是从整体负荷考虑。就是当某个服务熔断之后,服务器将不再被调用,此时客户端可以自己准备一个本地的fallback回调,返回一个缺省值。这样做,虽然水平下降,但好歹可用,比直接挂掉强。

    Hystrix相关注解@EnableHystrix:开启熔断 @HystrixCommand(fallbackMethod=”XXX”),声明一个失败回滚处理函数XXX,当被注解的方法执行超时(默认是1000毫秒),就会执行fallback函数,返回错误提示。

    6、请说说Eureka和zookeeper 的区别?

    Zookeeper保证了CP,Eureka保证了AP。

    A:高可用

    C:一致性

    P:分区容错性

    1.当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的信息,但不能容忍直接down掉不可用。也就是说,服务注册功能对高可用性要求比较高,但zk会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新选leader。问题在于,选取leader时间过长,30 ~ 120s,且选取期间zk集群都不可用,这样就会导致选取期间注册服务瘫痪。在云部署的环境下,因网络问题使得zk集群失去master节点是较大概率会发生的事,虽然服务能够恢复,但是漫长的选取时间导致的注册长期不可用是不能容忍的。

    2.Eureka保证了可用性,Eureka各个节点是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点仍然可以提供注册和查询服务。而Eureka的客户端向某个Eureka注册或发现时发生连接失败,则会自动切换到其他节点,只要有一台Eureka还在,就能保证注册服务可用,只是查到的信息可能不是最新的。除此之外,Eureka还有自我保护机制,如果在15分钟内超过85%的节点没有正常的心跳,那么Eureka就认为客户端与注册中心发生了网络故障,此时会出现以下几种情况:

    ①、Eureka不在从注册列表中移除因为长时间没有收到心跳而应该过期的服务。

    ②、Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其他节点上(即保证当前节点仍然可用)

    ③、当网络稳定时,当前实例新的注册信息会被同步到其他节点。

    因此,Eureka可以很好地应对因网络故障导致部分节点失去联系的情况,而不会像Zookeeper那样使整个微服务瘫痪

    7、SpringBoot和SpringCloud的区别?

    SpringBoot专注于快速方便得开发单个个体微服务。

    SpringCloud是关注全局的微服务协调整理治理框架,它将SpringBoot开发的一个个单体微服务整合并管理起来,

    为各个微服务之间提供,配置管理、服务发现、断路器、路由、微代理、事件总线、全局锁、决策竞选、分布式会话等等集成服务

    SpringBoot可以离开SpringCloud独立使用开发项目, 但是SpringCloud离不开SpringBoot ,属于依赖的关系.

    SpringBoot专注于快速、方便得开发单个微服务个体,SpringCloud关注全局的服务治理框架。

    8、负载平衡的意义什么?

    在计算中,负载平衡可以改善跨计算机,计算机集群,网络链接,中央处理单元或磁盘驱动器等多种计算资源的工作负载分布。负载平衡旨在优化资源使用,最大化吞吐量,最小化响应时间并避免任何单一资源 的过载。使用多个组件进行负载平衡而不是单个组件可能会通过冗余来提高可靠性和可用性。负载平衡通常涉及专用软件或硬件,例如多层交换机或域名系统服务器进程。

    9、什么是Hystrix?它如何实现容错?

    Hystrix是一个延迟和容错库,旨在隔离远程系统,服务和第三方库的访问点,当出现故障是不可避免的故障时,停止级联故障并在复杂的分布式系统中实现弹性。

    通常对于使用微服务架构开发的系统,涉及到许多微服务。这些微服务彼此协作。

    思考一下微服务:

    图片

    假设如果上图中的微服务9失败了,那么使用传统方法我们将传播一个异常。但这仍然会导致整个系统崩溃。

    随着微服务数量的增加,这个问题变得更加复杂。微服务的数量可以高达1000.这是hystrix出现的地方 我们将使用Hystrix在这种情况下的Fallback方法功能。我们有两个服务employee-consumer使用由employee-consumer公开的服务。

    简化图如下所示

    图片

    现在假设由于某种原因,employee-producer公开的服务会抛出异常。我们在这种情况下使用Hystrix定义了一个回退方法。这种后备方法应该具有与公开服务相同的返回类型。如果暴露服务中出现异常,则回退方法将返回一些值。

    10、什么是Hystrix断路器?我们需要它吗?

    由于某些原因,employee-consumer公开服务会引发异常。在这种情况下使用Hystrix我们定义了一个回退方法。如果在公开服务中发生异常,则回退方法返回一些默认值。

    图片

    如果firstPage method() 中的异常继续发生,则Hystrix电路将中断,并且员工使用者将一起跳过firtsPage方法,并直接调用回退方法。断路器的目的是给第一页方法或第一页方法可能调用的其他方法留出时间,并导致异常恢复。可能发生的情况是,在负载较小的情况下,导致异常的问题有更好的恢复机会 。

    图片

    11、说说 RPC 的实现原理

    首先需要有处理网络连接通讯的模块,负责连接建立、管理和消息的传输。其次需要有编 解码的模块,因为网络通讯都是传输的字节码,需要将我们使用的对象序列化和反序列 化。剩下的就是客户端和服务器端的部分,服务器端暴露要开放的服务接口,客户调用服 务接口的一个代理实现,这个代理实现负责收集数据、编码并传输给服务器然后等待结果 返回。

    12、eureka自我保护机制是什么?

    当Eureka Server 节点在短时间内丢失了过多实例的连接时(比如网络故障或频繁启动关闭客户端)节点会进入自我保护模式,保护注册信息,不再删除注册数据,故障恢复时,自动退出自我保护模式。

    13、什么是Ribbon?

    ribbon是一个负载均衡客户端,可以很好地控制htt和tcp的一些行为。feign默认集成了ribbon

    14、什么是 Netflix Feign?它的优点是什么?

    Feign 是受到 Retrofit,JAXRS-2.0 和 WebSocket 启发的 java 客户端联编程序。

    Feign 的第一个目标是将约束分母的复杂性统一到 http apis,而不考虑其稳定性。

    特点:

    • Feign 采用的是基于接口的注解
    • Feign 整合了ribbon,具有负载均衡的能力
    • 整合了Hystrix,具有熔断的能力

    使用方式

    • 添加pom依赖。
    • 启动类添加@EnableFeignClients
    • 定义一个接口@FeignClient(name=“xxx”)指定调用哪个服务

    15、Ribbon和Feign的区别?

    1.Ribbon都是调用其他服务的,但方式不同。2.启动类注解不同,Ribbon是@RibbonClient feign的是@EnableFeignClients 3.服务指定的位置不同,Ribbon是在@RibbonClient注解上声明,Feign则是在定义抽象方法的接口中使用@FeignClient声明。4.调用方式不同,Ribbon需要自己构建http请求,模拟http请求。

    16、Spring Cloud 的核心组件有哪些?

    • Eureka:服务注册于发现。
    • Feign:基于动态代理机制,根据注解和选择的机器,拼接请求 url 地址,发起请求。
    • Ribbon:实现负载均衡,从一个服务的多台机器中选择一台。
    • Hystrix:提供线程池,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题。
    • Zuul:网关管理,由 Zuul 网关转发请求给对应的服务。

    17、说说Spring Boot和Spring Cloud的关系

    Spring Boot是Spring推出用于解决传统框架配置文件冗余,装配组件繁杂的基于Maven的解决方案,旨在快速搭建单个微服务 而Spring Cloud专注于解决各个微服务之间的协调与配置,服务之间的通信,熔断,负载均衡等 技术维度并相同,并且Spring Cloud是依赖于Spring Boot的,而Spring Boot并不是依赖与Spring Cloud,甚至还可以和Dubbo进行优秀的整合开发

    总结

    • SpringBoot专注于快速方便的开发单个个体的微服务
    • SpringCloud是关注全局的微服务协调整理治理框架,整合并管理各个微服务,为各个微服务之间提供,配置管理,服务发现,断路器,路由,事件总线等集成服务
    • Spring Boot不依赖于Spring Cloud,Spring Cloud依赖于Spring Boot,属于依赖关系
    • Spring Boot专注于快速,方便的开发单个的微服务个体,Spring Cloud关注全局的服务治理框架

    18、说说微服务之间是如何独立通讯的?

    远程过程调用(Remote Procedure Invocation)

    也就是我们常说的服务的注册与发现,直接通过远程过程调用来访问别的service。

    优点 :简单,常见,因为没有中间件代理,系统更简单

    缺点 :只支持请求/响应的模式,不支持别的,比如通知、请求/异步响应、发布/订阅、发布/异步响应,降低了可用性,因为客户端和服务端在请求过程中必须都是可用的。

    消息

    使用异步消息来做服务间通信。服务间通过消息管道来交换消息,从而通信。

    优点 :把客户端和服务端解耦,更松耦合,提高可用性,因为消息中间件缓存了消息,直到消费者可以消费,   支持很多通信机制比如通知、请求/异步响应、发布/订阅、发布/异步响应。

    缺点 :消息中间件有额外的复杂。

    19、Spring Cloud如何实现服务的注册?

    服务发布时,指定对应的服务名,将服务注册到 注册中心(Eureka 、Zookeeper)

    注册中心加@EnableEurekaServer,服务用@EnableDiscoveryClient,然后用ribbon或feign进行服务直接的调用发现。

    此题偏向于向实战,就看你是不是背面试题的,没有实战的人是不知道的。

    20、什么是服务熔断?

    在复杂的分布式系统中,微服务之间的相互调用,有可能出现各种各样的原因导致服务的阻塞,在高并发场景下,服务的阻塞意味着线程的阻塞,导致当前线程不可用,服务器的线程全部阻塞,导致服务器崩溃,由于服务之间的调用关系是同步的,会对整个微服务系统造成服务雪崩

    为了解决某个微服务的调用响应时间过长或者不可用进而占用越来越多的系统资源引起雪崩效应就需要进行服务熔断和服务降级处理。

    所谓的服务熔断指的是某个服务故障或异常一起类似显示世界中的“保险丝”当某个异常条件被触发就直接熔断整个服务,而不是一直等到此服务超时。

    服务熔断就是相当于我们电闸的保险丝,一旦发生服务雪崩的,就会熔断整个服务,通过维护一个自己的线程池,当线程达到阈值的时候就启动服务降级,如果其他请求继续访问就直接返回fallback的默认值

    21、了解Eureka自我保护机制吗?

    当Eureka Server 节点在短时间内丢失了过多实例的连接时(比如网络故障或频繁启动关闭客户端)节点会进入自我保护模式,保护注册信息,不再删除注册数据,故障恢复时,自动退出自我保护模式。

    22、熟悉 Spring Cloud Bus 吗?

    spring cloud bus 将分布式的节点用轻量的消息代理连接起来,它可以用于广播配置文件的更改或者服务直接的通讯,也可用于监控。如果修改了配置文件,发送一次请求,所有的客户端便会重新读取配置文件。

    23、Spring Cloud 断路器有什么作用?

    当一个服务调用另一个服务由于网络原因或自身原因出现问题,调用者就会等待被调用者的响应,当更多的服务请求到这些资源导致更多的请求等待,发生连锁效应(雪崩效应)。一段时间内 达到一定的次数无法调用 并且多次监测没有恢复的迹象,这时候断路器完全打开 那么下次请求就不会请求到该服务。

    半开:短时间内 有恢复迹象 断路器会将部分请求发给该服务,正常调用时 断路器关闭。关闭:当服务一直处于正常状态 能正常调用。

    24、了解Spring Cloud Config 吗?

    在分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管理,实时更新,所以需要分布式配置中心组件。在Spring Cloud中,有分布式配置中心组件Spring Cloud Config,它支持配置服务放在配置服务的内存中(即本地),也支持放在远程Git仓库中。

    Spring Cloud Config 组件中,分两个角色,一是config server,二是config client。

    使用方式:

    • 添加pom依赖
    • 配置文件添加相关配置
    • 启动类添加注解@EnableConfigServer

    25、说说你对Spring Cloud Gateway的理解

    Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代Zuul网关。网关作为流量的,在微服务系统中有着非常作用,网关常见的功能有路由转发、权限校验、限流控制等作用。

    使用了一个RouteLocatorBuilder的bean去创建路由,除了创建路由RouteLocatorBuilder可以让你添加各种predicates和filters,predicates断言的意思,顾名思义就是根据具体的请求的规则,由具体的route去处理,filters是各种过滤器,用来对请求做各种判断和修改。

    参考;http://1pgqu.cn/M0NZo

    总结

    Spring Cloud目前相当的火热,也差不多是java开发者必备技能之一了。面试的时候被问,那也是正常不过了,很多人可能用来很久,但是没有去了解过原理,面试照样挂掉。背面试题,在很大层面上还是很有用的。但从长远角度来说,希望大家更深层次去学习、去实践。只有自己真的掌握,那才叫NB。

    
    

  • 还在手动配置Nginx?试试这款可视化管理工具吧,用起来够优雅!

    快乐分享,Java干货及时送达👇

    今天给大家介绍一款 Nginx 可视化管理界面,非常好用,小白也能立马上手。

    nginx-proxy-manager 是一个反向代理管理系统,它基于 NGINX,具有漂亮干净的 Web UI。还可以获得受信任的 SSL 证书,并通过单独的配置、自定义和入侵保护来管理多个代理。它是开源的,斩获 11.8K 的 Star 数。

    特征

    • 基于 Tabler(https://tabler.github.io/) 的美观安全的管理界面
    • 无需了解 Nginx 即可轻松创建转发域、重定向、流和 404 主机
    • 使用 Let’s Encrypt 的免费 SSL 或提供您自己的自定义 SSL 证书
    • 主机的访问列表和基本 HTTP 身份验证
    • 高级 Nginx 配置可供超级用户使用
    • 用户管理、权限和审核日志

    安装

    1、安装 Docker 和 Docker-Compose

    2、创建一个docker-compose.yml文件

    version: '3'
    services:
      app:
        image: 'jc21/nginx-proxy-manager:latest'
        restart: unless-stopped
        ports:
          - '80:80'
          - '81:81'
          - '443:443'
        volumes:
          - ./data:/data
          - ./letsencrypt:/etc/letsencrypt

    3、运行

    docker-compose up -d

    #如果使用的是 docker-compose-plugin
    docker compose up -d

    4、访问网页

    运行成功后,访问 http://127.0.0.1:81 就能看到界面啦

    5、登录

    网站默认账号和密码为

    账号:admin@example.com
    密码:changeme

    登录成功后第一次要求修改密码,按照步骤修改即可!

    6、登录成功主界面

    实战:设置后台管理界面的反向代理

    这里,我们就用 http://a.test.com 来绑定我们的端口号为81的后台管理界面,实现浏览器输入 http://a.test.com 即可访问后台管理界面,并且设置HTTPS。

    1、前提

    • 安装好Nginx Proxy Manager
    • 拥有一个域名
    • 将 http://a.test.com 解析到安装Nginx Proxy Manager的服务器ip地址上

    2、反向代理操作

    先用ip:81 访问后台管理界面,然后输入账号密码进入后台。

    点击绿色图标的选项

    点击右边Add Proxy Host ,在弹出的界面Details选项中填写相应的字段。

    • Domain Names: 填写要反向代理的域名,这里就是http://a.test.com
    • Forward Hostname / IP: 填写的ip值见下文解释
    • Forward Port: 反向代理的端口,这里就是81
    • Block Common Exploits: 开启后阻止一些常见漏洞
    • 其余两个暂不知作用

    Forward Hostname / IP填写说明

    如果搭建的服务和nginx proxy manager服务所在不是一个服务器,则填写能访问对应服务的IP。如果都在同一台服务器上,则填写在服务器中输入ip addr show docker0 命令获取得到的ip。

    这里不填127.0.0.1的原因是使用的是docker容器搭建web应用,docker容器和宿主机即服务器不在同一个网络下,所以127.0.0.1并不能访问到宿主机,而ip addr show docker0获得的ip地址就是宿主机地址。

    接下来即可用a.test.com 访问后台管理界面,此时还只是http协议,没有https。不过此时就可以把之前的81端口关闭了,输入a.test.com 访问的是服务器80端口,然后在转发给内部的81端口。

    3、申请ssl证书

    申请一个a.test.com 证书,这样就可以提供https访问了。

    在Nginx Proxy Manager管理后台,选择Access Lists->Add SSL Certificate->Let's Encrypt选项。

    按照下图方式填写,点击Save就可以了

    4、设置HTTPS

    进入反向代理设置界面,编辑上文创建的反代服务,选择SSL选项,下拉菜单中选择我们申请的证书,然后可以勾选Force SSL即强制HTTPS。

    总结

    以上就是本教程的全部内容,更多的使用教程,大家可以访问官方文档。

    官方文档:https://nginxproxymanager.com/guide/