无常是常

分享技术见解、学习心得和生活感悟

最新文章

数据复制

数据复制方案,人们通常希望达到以下几个目的:

  • 使数据在地理位置上更接近用户,从而降低访问延迟。
  • 当部分组件出现故障,系统依然可以继续工作,从而提升可用性。
  • 扩展至多台机器以同时提供数据访问服务,从而提高吞吐量。

三种比较流行的复制数据变化的方法:

  • 主从复制
  • 多主节点复制
  • 无主节点复制

复制技术存在很多折中的方案,例如:同步复制和异步复制,一般情况数据库会采用配置的形式来处理这些策略。

主节点与从节点

主从复制的工作原理如下:

  1. 指定一个副本为主节点,当客户写数据库时,只能通过主节点进行写入。
  2. 其他副本作为从副本,主副本将数据写入本地存储后,将数据更改作为复制日志或更改流发送到从副本。每个副本获取到更改数据后将其应用到本地,并且严格保持与主副本相同的写入顺序。
  3. 客户端读取数据时从主副本或者从副本进行读取,从客户端的角度看,从副本都是只读的。

同步复制与异步复制

同步复制是需要等从节点确认完成了写入之后,才会向用户报告完成。并将最新的写入对其他客户端可见。异步复制是主节点发送完消息之后立即返回,不需要等待从节点完成确认。

同步复制的优点

一旦向用户确认,从节点可以明确保证完成了与主节点的更新同步,数据已处于最新版本。万一主节点发生故障,总是可以在从节点继续访问最新数据。

同步复制的缺点

如果同步的从节点无法完成确认(例如由于从节点发送崩溃、网络故障或其他原因),写入就不能视为成功。主节点会阻塞其后面所有的写操作,直到同步副本确认完成。

把所有从节点都设置为同步复制有些不切实际,实践中,可以将某一个节点设置为同步复制,其他节点设置为异步复制。万一同步复制的节点变得不可用或者性能下降,则将另外一个节点从异步模式提升为同步模式,这样可以保证至少有两个节点拥有最新的数据副本。这种配置有时称为半同步

异步复制的优点

不管从节点的数据多么滞后,主节点总是可以继续响应客户端的写请求,系统的吞吐性能更好。

异步复制的缺点

如果主节点发生故障并且不可恢复,则所有尚未复制到从节点的数据将会丢失,这就意味着向客户端确认了写请求,但是却无法保证数据的持久化。

配置新的从节点

如何考虑添加新的从节点,怎么保证主从数据一致性呢?

  1. 在某个时间节点对主节点的数据副本生成一个一致性快照。
  2. 将快照拷贝到新的从节点。
  3. 从节点连接到主节点并请求快照点之后所发生的数据更改日志。因为在第一步创建快照时,快照与系统复制日志的某个确定的位置相关联。
  4. 获得日志之后,从节点应用这些快照点之后的所有数据变更,这个过程称之为追赶。接下来,他可以继续处理主节点上的新的数据变化。并重复1~4步骤。

从节点失效:追赶式恢复

从节点如果发生了崩溃或者网络闪断,则根据最后一笔事务的处理日志,从主节点拉取之后的所有数据的变更,收到所有数据的变更后,将其应用到本地用于追赶主节点,之后就和正常情况一样持续接收来自主节点数据流的变化。

主节点失效:节点切换

  1. 确定主节点失效。大部分系统都采用了基于超时的机制判断节点是否失效,节点间频繁地互相发送心跳存活信息,如果某一个节点在一段时间内(例如30s)没有响应,则认为该节点发生了实效。
  2. 选举新的主节点。可以通过选举的方式来选举主节点,候选节点最好与主节点的数据差异最小,这样可以最小化数据丢失的风险。
  3. 重新配置系统使新的主节点生效。如果原主节点重新上线后,可能仍然认为自己是主节点,这是系统要确保原主节点降级为从节点,并认可新的主节点。

以上切换过程可能会发生很多变数

  • 如果使用了异步复制,且失效之前,新的主节点并未收到原主节点的所有数据;在选举之后,原主节点很快又重新上线加入到集群,这是可能原主节点并未意识到角色的变化,还会尝试同步其他从节点,但其中的一个现在已经接管成为现任主节点。常见的解决方案是:原主节点上未完成复制的写请求就此丢弃,但这可能会违背数据更新持久化的承诺。

  • 如果数据库依赖于外部系统(例如Redis)一起协同使用,丢弃数据的方案就特别危险。例如,在GithHub的一次事故中,某个数据并完全的MySQL从节点提升为主节点,数据库使用了基于自增计数器将主键分配给新创建的行,但是因为新的主节点计数器落后于原主节点(即二者并非完全同步),它重新使用了已被原主节点分配出去的某些主键,而恰好这些主键已被外部Redis所引用,结果出现MySQL和Redis之间的不一致,最后导致了某些私有数据被错误的泄露给了其它用户。

  • 某些情况下,可能会发生两个节点同时认为自己是主节点,这种情况下被称为脑裂,它非常危险:两个主节点都可能接收写请求,并且没有很好解决冲突的办法,最后数据可能会丢失或者破坏,一种安全应急方案会强制关闭其中的一个节点。

  • 如何设置合适的超时来检查主节点失效?主节点失效后,超时时间设置的越长,也就意味着数据恢复的时间越长。如果设置的太短,可能会导致很多不必要的节点切换。例如:突然的负载峰值会导致节点的响应时间变长甚至超时,或者由于网络故障导致延迟增加。如果系统本身已经处于高负载或网络严重拥塞的情况下,不必要的切换只会导致系统的情况变得更糟。

复制日志的实现

基于语句的复制

一些不适用的场景:

  • 任何调用非确定性的语句,如NOW()获取当前时间,或RAND()获取一个随机数等,可能会在不同副本产生不同的值。

  • 如果语句中使用自增列,或者依赖数据库的现有数据(例如:UPDATE … WHERE … <某些条件>),则所有副本必须按照相同的顺序执行,否则可能会带来不同的结果。

  • 有副作用的语句(例如:触发器、存储过程、用户自定义的函数等),可能会在每个副本产生不同的副作用。

可能解决的方案是将不确定的函数替换成确定的结果,不过这种方式仍有许多地方需要考虑。

基于预写日志(WAL)传输

  • 对于日志结构的存储引擎(例如:SSTables和LSM-trees),日志是主要的存储方式。日志段在后台压缩并支持垃圾回收。

  • 对于采用覆盖写磁盘的Btree结构,每次修改会预先写入日志,如果系统崩溃,通过索引更新的方式迅速恢复到此前一直状态。

缺点:

WAL包含了哪些磁盘块的哪些字节发生了改变,诸如此类细节,这使得复制方案和存储引擎紧密耦合。如果数据库的存储格式从一个版本改为另一个版本,那么系统通常无法支持主从节点上运行不同版本的软件。

基于行的逻辑日志复制

关系数据库的逻辑日志通常是指一些列记录数据行级别的写请求:

  • 对行的插入,日志包含所有相关列的新值。

  • 对于行的删除,日志有足够的的信息来唯一标识已删除的行。

  • 对于行的更新,日志包含足够的信息来唯一标识更新的行,以及所有列的新值。

MySQL的二进制日志binlog(当配置基于行的复制时)使用该方式。

基于触发器的复制

基于触发器复制支持更高的灵活性。例如将一种数据库的数据复制到另外一种数据库。触发器支持注册自己的应用层代码,使数据库发生数据更改时自动执行自定义代码。 基于触发器的复制通常比其他方式复制开销更高,也比数据库内置复制更容易出错或者暴露一些限制。

复制滞后的问题

由于并非所有写入都反映到副本上,如果同时对主节点和从节点发起相同的查询,可能会得到不同的接口,这种不一致的状态只是暂时的,可能经过一段时间后,从节点最终会赶上主节点并与主节点保持一致。这种效应也被称为最终一致性。

读自己的写

读自己的写也被称为读写一致性。实现读写一致性有多重可行方案:

  • 总是从主节点读取当前用户自己的数据,而从从节点读取其他用户的数据。

  • 跟踪数据最近更新时间,如果数据更新后一分钟内总是从主节点读取数据,并监控从节点复制之后的程度,避免从那些滞后时间超过一分钟的从节点读取。

  • 客户端记住最近更新时的时间戳,并附带到请求中,根据此信息,系统可以保证对该用户提供读服务时都应该至少包含了该时间戳的更新。如果不够新,则交给其他副本进行处理,要么等待直到副本接收到了最近的更新。

  • 如果副本分布在多数据中心,必须先把请求路由到主节点所在的数据中心。

单调读

主要是解决用户看到了最新内容之后又读到了过期的内容,好像时间被回拨,此时需要单调读一致性。(出现这种情况主要是因为主从节点数据不一致或同步滞后导致的)

解决方案:确保每个用户总是从固定的同一副本执行读取。(例如:基于用户Id进行哈希的方式选择副本,而不是随机选择)

前缀一致性

在许多分布式数据库中,不同分区独立运行,因此不存在写入顺序。这就导致当用户从数据库中读取数据时,可能会看到数据库的某部分新值和另一部分旧值。

一个解决方案是确保任何具有因果顺序关系的写入都交给一个分区来完成,但这种方案真是实现效率会大打折扣。

数据复制方案,人们通常希望达到以下几个目的:

  • 使数据在地理位置上更接近用户,从而降低访问延迟。
  • 当部分组件出现故障,系统依然可以继续工作,从而提升可用性。
  • 扩展至多台机器以同时提供数据访问服务,从而提高吞吐量。

三种比较流行的复制数据变化的方法:

  • 主从复制
  • 多主节点复制
  • 无主节点复制

复制技术存在很多折中的方案,例如:同步复制和异步复制,一般情况数据库会采用配置的形式来处理这些策略。

主节点与从节点

主从复制的工作原理如下:

  1. 指定一个副本为主节点,当客户写数据库时,只能通过主节点进行写入。
  2. 其他副本作为从副本,主副本将数据写入本地存储后,将数据更改作为复制日志或更改流发送到从副本。每个副本获取到更改数据后将其应用到本地,并且严格保持与主副本相同的写入顺序。
  3. 客户端读取数据时从主副本或者从副本进行读取,从客户端的角度看,从副本都是只读的。

同步复制与异步复制

同步复制是需要等从节点确认完成了写入之后,才会向用户报告完成。并将最新的写入对其他客户端可见。异步复制是主节点发送完消息之后立即返回,不需要等待从节点完成确认。

同步复制的优点

一旦向用户确认,从节点可以明确保证完成了与主节点的更新同步,数据已处于最新版本。万一主节点发生故障,总是可以在从节点继续访问最新数据。

同步复制的缺点

如果同步的从节点无法完成确认(例如由于从节点发送崩溃、网络故障或其他原因),写入就不能视为成功。主节点会阻塞其后面所有的写操作,直到同步副本确认完成。

把所有从节点都设置为同步复制有些不切实际,实践中,可以将某一个节点设置为同步复制,其他节点设置为异步复制。万一同步复制的节点变得不可用或者性能下降,则将另外一个节点从异步模式提升为同步模式,这样可以保证至少有两个节点拥有最新的数据副本。这种配置有时称为半同步

异步复制的优点

不管从节点的数据多么滞后,主节点总是可以继续响应客户端的写请求,系统的吞吐性能更好。

异步复制的缺点

如果主节点发生故障并且不可恢复,则所有尚未复制到从节点的数据将会丢失,这就意味着向客户端确认了写请求,但是却无法保证数据的持久化。

配置新的从节点

如何考虑添加新的从节点,怎么保证主从数据一致性呢?

  1. 在某个时间节点对主节点的数据副本生成一个一致性快照。
  2. 将快照拷贝到新的从节点。
  3. 从节点连接到主节点并请求快照点之后所发生的数据更改日志。因为在第一步创建快照时,快照与系统复制日志的某个确定的位置相关联。
  4. 获得日志之后,从节点应用这些快照点之后的所有数据变更,这个过程称之为追赶。接下来,他可以继续处理主节点上的新的数据变化。并重复1~4步骤。

从节点失效:追赶式恢复

从节点如果发生了崩溃或者网络闪断,则根据最后一笔事务的处理日志,从主节点拉取之后的所有数据的变更,收到所有数据的变更后,将其应用到本地用于追赶主节点,之后就和正常情况一样持续接收来自主节点数据流的变化。

主节点失效:节点切换

  1. 确定主节点失效。大部分系统都采用了基于超时的机制判断节点是否失效,节点间频繁地互相发送心跳存活信息,如果某一个节点在一段时间内(例如30s)没有响应,则认为该节点发生了实效。
  2. 选举新的主节点。可以通过选举的方式来选举主节点,候选节点最好与主节点的数据差异最小,这样可以最小化数据丢失的风险。
  3. 重新配置系统使新的主节点生效。如果原主节点重新上线后,可能仍然认为自己是主节点,这是系统要确保原主节点降级为从节点,并认可新的主节点。

以上切换过程可能会发生很多变数

  • 如果使用了异步复制,且失效之前,新的主节点并未收到原主节点的所有数据;在选举之后,原主节点很快又重新上线加入到集群,这是可能原主节点并未意识到角色的变化,还会尝试同步其他从节点,但其中的一个现在已经接管成为现任主节点。常见的解决方案是:原主节点上未完成复制的写请求就此丢弃,但这可能会违背数据更新持久化的承诺。

  • 如果数据库依赖于外部系统(例如Redis)一起协同使用,丢弃数据的方案就特别危险。例如,在GithHub的一次事故中,某个数据并完全的MySQL从节点提升为主节点,数据库使用了基于自增计数器将主键分配给新创建的行,但是因为新的主节点计数器落后于原主节点(即二者并非完全同步),它重新使用了已被原主节点分配出去的某些主键,而恰好这些主键已被外部Redis所引用,结果出现MySQL和Redis之间的不一致,最后导致了某些私有数据被错误的泄露给了其它用户。

  • 某些情况下,可能会发生两个节点同时认为自己是主节点,这种情况下被称为脑裂,它非常危险:两个主节点都可能接收写请求,并且没有很好解决冲突的办法,最后数据可能会丢失或者破坏,一种安全应急方案会强制关闭其中的一个节点。

  • 如何设置合适的超时来检查主节点失效?主节点失效后,超时时间设置的越长,也就意味着数据恢复的时间越长。如果设置的太短,可能会导致很多不必要的节点切换。例如:突然的负载峰值会导致节点的响应时间变长甚至超时,或者由于网络故障导致延迟增加。如果系统本身已经处于高负载或网络严重拥塞的情况下,不必要的切换只会导致系统的情况变得更糟。

复制日志的实现

基于语句的复制

一些不适用的场景:

  • 任何调用非确定性的语句,如NOW()获取当前时间,或RAND()获取一个随机数等,可能会在不同副本产生不同的值。

  • 如果语句中使用自增列,或者依赖数据库的现有数据(例如:UPDATE … WHERE … <某些条件>),则所有副本必须按照相同的顺序执行,否则可能会带来不同的结果。

  • 有副作用的语句(例如:触发器、存储过程、用户自定义的函数等),可能会在每个副本产生不同的副作用。

可能解决的方案是将不确定的函数替换成确定的结果,不过这种方式仍有许多地方需要考虑。

基于预写日志(WAL)传输

  • 对于日志结构的存储引擎(例如:SSTables和LSM-trees),日志是主要的存储方式。日志段在后台压缩并支持垃圾回收。

  • 对于采用覆盖写磁盘的Btree结构,每次修改会预先写入日志,如果系统崩溃,通过索引更新的方式迅速恢复到此前一直状态。

缺点:

WAL包含了哪些磁盘块的哪些字节发生了改变,诸如此类细节,这使得复制方案和存储引擎紧密耦合。如果数据库的存储格式从一个版本改为另一个版本,那么系统通常无法支持主从节点上运行不同版本的软件。

基于行的逻辑日志复制

关系数据库的逻辑日志通常是指一些列记录数据行级别的写请求:

  • 对行的插入,日志包含所有相关列的新值。

  • 对于行的删除,日志有足够的的信息来唯一标识已删除的行。

  • 对于行的更新,日志包含足够的信息来唯一标识更新的行,以及所有列的新值。

MySQL的二进制日志binlog(当配置基于行的复制时)使用该方式。

基于触发器的复制

基于触发器复制支持更高的灵活性。例如将一种数据库的数据复制到另外一种数据库。触发器支持注册自己的应用层代码,使数据库发生数据更改时自动执行自定义代码。 基于触发器的复制通常比其他方式复制开销更高,也比数据库内置复制更容易出错或者暴露一些限制。

复制滞后的问题

由于并非所有写入都反映到副本上,如果同时对主节点和从节点发起相同的查询,可能会得到不同的接口,这种不一致的状态只是暂时的,可能经过一段时间后,从节点最终会赶上主节点并与主节点保持一致。这种效应也被称为最终一致性。

读自己的写

读自己的写也被称为读写一致性。实现读写一致性有多重可行方案:

  • 总是从主节点读取当前用户自己的数据,而从从节点读取其他用户的数据。

  • 跟踪数据最近更新时间,如果数据更新后一分钟内总是从主节点读取数据,并监控从节点复制之后的程度,避免从那些滞后时间超过一分钟的从节点读取。

  • 客户端记住最近更新时的时间戳,并附带到请求中,根据此信息,系统可以保证对该用户提供读服务时都应该至少包含了该时间戳的更新。如果不够新,则交给其他副本进行处理,要么等待直到副本接收到了最近的更新。

  • 如果副本分布在多数据中心,必须先把请求路由到主节点所在的数据中心。

单调读

主要是解决用户看到了最新内容之后又读到了过期的内容,好像时间被回拨,此时需要单调读一致性。(出现这种情况主要是因为主从节点数据不一致或同步滞后导致的)

解决方案:确保每个用户总是从固定的同一副本执行读取。(例如:基于用户Id进行哈希的方式选择副本,而不是随机选择)

前缀一致性

在许多分布式数据库中,不同分区独立运行,因此不存在写入顺序。这就导致当用户从数据库中读取数据时,可能会看到数据库的某部分新值和另一部分旧值。

一个解决方案是确保任何具有因果顺序关系的写入都交给一个分区来完成,但这种方案真是实现效率会大打折扣。

程序员的测试课

时代在要求我们写测试

因为软件变得越来越复杂,测试可以让我们在复杂的软件开发中稳步前行。另一方面软件测试可以让我们在长期的过程中不断回归,让每一步走的更稳。

程序员圈子流传着一个关于测试的段子:** 每个程序员在修改代码时都希望有测试,而在写代码时,都不想写测试。**

大部分程序员都不会写测试

很多程序员反对写测试,本质上的原因是因为他们不会写测试。

你的代码质量真的高吗?

  • 经过测试的代码,质量会更高;
  • 要想写好测试,代码本身质量也要高。

如果你连测试都做不好,你对自己代码的信心从何而来呢?

学习写测试

最好的办法就是跟着会写测试的人一起写一段时间

思考:可以查阅优秀的开源代码是如何写测试的。


ToDo项目的一些基本准备工作

  • 一个项目的自动化;
  • 对需求进行简单设计。

为什么需要自动化呢?简单来说是为了防止一些低级错误。

把核心的业务部分和命令行呈现的部分分开。

任务分解

从离我们需求最近的入口开始。

要想测试一个函数,一个函数最好是可测的。什么是可测的?就是通过函数的接口设计,我们给出特定的输入,它能给我们相应的输出。所以,一个函数最好是有返回值的。

Fail Fast 原则
一条设计规范:对于输入参数的检测,由入口部分代码进行处理。
一条设计规范:Repository 的问题以运行时异常的形式抛出,业务层不需要做任何处理。

项目刚开始时,我们要准备哪些内容:

  • 项目的自动化;
  • 针对需求进行初步的设计。

着手编写代码时,我们要怎么做呢?

  • 对要实现的需求进行任务分解;
  • 在一个具体的需求任务中,我们可以从需求入口开始入手;
  • 设计一个可测试的函数;
  • 针对具体的函数,考虑测试场景;
  • 针对具体的测试场景,将场景具象化成测试用例。

在梳理的过程中,我们还会针对一些统一的情况作出一些约定,成为项目整体的设计规范,比如,在这里我们约定:

  • 对于输入参数的检测,由入口部分代码进行处理;
  • Repository 的问题以运行时异常的形式抛出,业务层不需要做任何处理。

在编码的过程中,我们也看到了:

  • 根据不断增加的需求,逐渐改动我们的设计,这就是演化式设计的基本做法;
  • 我们对待测试也像对待代码一样,会消除代码中存在的一些坏味道。

时代在要求我们写测试

因为软件变得越来越复杂,测试可以让我们在复杂的软件开发中稳步前行。另一方面软件测试可以让我们在长期的过程中不断回归,让每一步走的更稳。

程序员圈子流传着一个关于测试的段子:** 每个程序员在修改代码时都希望有测试,而在写代码时,都不想写测试。**

大部分程序员都不会写测试

很多程序员反对写测试,本质上的原因是因为他们不会写测试。

你的代码质量真的高吗?

  • 经过测试的代码,质量会更高;
  • 要想写好测试,代码本身质量也要高。

如果你连测试都做不好,你对自己代码的信心从何而来呢?

学习写测试

最好的办法就是跟着会写测试的人一起写一段时间

思考:可以查阅优秀的开源代码是如何写测试的。


ToDo项目的一些基本准备工作

  • 一个项目的自动化;
  • 对需求进行简单设计。

为什么需要自动化呢?简单来说是为了防止一些低级错误。

把核心的业务部分和命令行呈现的部分分开。

任务分解

从离我们需求最近的入口开始。

要想测试一个函数,一个函数最好是可测的。什么是可测的?就是通过函数的接口设计,我们给出特定的输入,它能给我们相应的输出。所以,一个函数最好是有返回值的。

Fail Fast 原则
一条设计规范:对于输入参数的检测,由入口部分代码进行处理。
一条设计规范:Repository 的问题以运行时异常的形式抛出,业务层不需要做任何处理。

项目刚开始时,我们要准备哪些内容:

  • 项目的自动化;
  • 针对需求进行初步的设计。

着手编写代码时,我们要怎么做呢?

  • 对要实现的需求进行任务分解;
  • 在一个具体的需求任务中,我们可以从需求入口开始入手;
  • 设计一个可测试的函数;
  • 针对具体的函数,考虑测试场景;
  • 针对具体的测试场景,将场景具象化成测试用例。

在梳理的过程中,我们还会针对一些统一的情况作出一些约定,成为项目整体的设计规范,比如,在这里我们约定:

  • 对于输入参数的检测,由入口部分代码进行处理;
  • Repository 的问题以运行时异常的形式抛出,业务层不需要做任何处理。

在编码的过程中,我们也看到了:

  • 根据不断增加的需求,逐渐改动我们的设计,这就是演化式设计的基本做法;
  • 我们对待测试也像对待代码一样,会消除代码中存在的一些坏味道。

我每天使用的11个Git Commands

本文是为我的年轻人(或任何新手)准备的,他们可以有效地使用git命令行,并简要介绍了我如何使用这些命令。
https://medium.com/@mmpatil34/11-git-commands-i-use-daily-9bbd7590c8eb

1. git fetch origin

从特定的仓库拉取所有的branchs/tags, 这里的仓库是“origin”, 我每天从这个命令开始, 它可以让本地和远程仓库状态保持一致.

2. git status

显示当前分支自上次提交到现在的文件改动列表,在切换分支、创建新分支、进行新更改或拉取更改之前, 此命令可以检查是否有文件需要被stash.

3. git checkout

git checkout -b <new branch name> origin/<source branch name>
从特定源分支创建一个新分支,这块的“origin”代表默认仓库.
git checkout — — <name of the file>
当本地有文件变动时,可以使用这个命令丢弃当前改变,恢复文件到之前状态.
git checkout <branch name>
切换本地到指定分支.

4. git pull origin

将更改从远程分支拉到本地分支,并在更改兼容的情况下调用git merge
git pull 和 git fetch的不同之处是: git pull = git fetch + git merge

5. git add

文件修改完成后,就可以使用git add命令将文件添加到特定提交中,使用git status命令可以很方便的获取到要添加指定提交的文件名.

6. git commit

git commit -m "<提交内容的描述>"
提交本地改变,并指定和提交内容相关的描述.

7. git push origin

将本地提交推送到远程存储库,这里的仓库是“origin”.

8. git cherry-pick

1
2
3
a - b - c - d   Master
\
e - f - g Feature

现在将提交f应用到master分支。

1
2
3
4
5
6

# 切换到 master 分支
$ git checkout master

# Cherry pick 操作
$ git cherry-pick f

上面的操作完成以后,代码库就变成了下面的样子。

1
2
3
a - b - c - d - f   Master
\
e - f - g Feature
1
git cherry-pick -m 1 <commit_id>

如果原始提交是一个合并节点,来自于两个分支的合并,那么 Cherry pick 默认将失败,因为它不知道应该采用哪个分支的代码变动。

-m配置项告诉 Git,应该采用哪个分支的变动。它的参数parent-number是一个从1开始的整数,代表原始提交的父分支编号。

上面命令表示,Cherry pick 采用提交commitHash来自编号1的父分支的变动。
一般来说,1号父分支是接受变动的分支(the branch being merged into),2号父分支是作为变动来源的分支(the branch being merged from)。

9. git revert

引入一个新的提交来撤回已经push的提交.

10. git reset — soft HEAD~1

撤消一次本地提交而不会丢失文件中的变更.

11. git reset — hard HEAD~1

撤销一次本地提交并且丢弃文件中的变更.
希望对你有帮助. 感谢阅读😁

本文是为我的年轻人(或任何新手)准备的,他们可以有效地使用git命令行,并简要介绍了我如何使用这些命令。
https://medium.com/@mmpatil34/11-git-commands-i-use-daily-9bbd7590c8eb

1. git fetch origin

从特定的仓库拉取所有的branchs/tags, 这里的仓库是“origin”, 我每天从这个命令开始, 它可以让本地和远程仓库状态保持一致.

2. git status

显示当前分支自上次提交到现在的文件改动列表,在切换分支、创建新分支、进行新更改或拉取更改之前, 此命令可以检查是否有文件需要被stash.

3. git checkout

git checkout -b <new branch name> origin/<source branch name>
从特定源分支创建一个新分支,这块的“origin”代表默认仓库.
git checkout — — <name of the file>
当本地有文件变动时,可以使用这个命令丢弃当前改变,恢复文件到之前状态.
git checkout <branch name>
切换本地到指定分支.

4. git pull origin

将更改从远程分支拉到本地分支,并在更改兼容的情况下调用git merge
git pull 和 git fetch的不同之处是: git pull = git fetch + git merge

5. git add

文件修改完成后,就可以使用git add命令将文件添加到特定提交中,使用git status命令可以很方便的获取到要添加指定提交的文件名.

6. git commit

git commit -m "<提交内容的描述>"
提交本地改变,并指定和提交内容相关的描述.

7. git push origin

将本地提交推送到远程存储库,这里的仓库是“origin”.

8. git cherry-pick

1
2
3
a - b - c - d   Master
\
e - f - g Feature

现在将提交f应用到master分支。

1
2
3
4
5
6

# 切换到 master 分支
$ git checkout master

# Cherry pick 操作
$ git cherry-pick f

上面的操作完成以后,代码库就变成了下面的样子。

1
2
3
a - b - c - d - f   Master
\
e - f - g Feature
1
git cherry-pick -m 1 <commit_id>

如果原始提交是一个合并节点,来自于两个分支的合并,那么 Cherry pick 默认将失败,因为它不知道应该采用哪个分支的代码变动。

-m配置项告诉 Git,应该采用哪个分支的变动。它的参数parent-number是一个从1开始的整数,代表原始提交的父分支编号。

上面命令表示,Cherry pick 采用提交commitHash来自编号1的父分支的变动。
一般来说,1号父分支是接受变动的分支(the branch being merged into),2号父分支是作为变动来源的分支(the branch being merged from)。

9. git revert

引入一个新的提交来撤回已经push的提交.

10. git reset — soft HEAD~1

撤消一次本地提交而不会丢失文件中的变更.

11. git reset — hard HEAD~1

撤销一次本地提交并且丢弃文件中的变更.
希望对你有帮助. 感谢阅读😁

go反向代理quickstart

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package main

import (
"context"
"github.com/gorilla/mux"
"github.com/txn2/txeh"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/signal"
)

var hostArr = []string{"project-01", "project-02"}

const (
host = "api-dev.deveye.cn"
proxyUrl = "https://api-dev.deveye.cn/"
address = "127.0.0.1"
)

func main() {
hosts, err := txeh.NewHostsDefault()
if err != nil {
panic(err)
}
hosts.AddHosts(address, hostArr)
log.Println(hosts.Save())
r := mux.NewRouter()
r.Use(LoggingHandler, AuthHandler, RecoverHandler)
r.PathPrefix("/").HandlerFunc(ReverseProxy)
srv := &http.Server{
Addr: ":80",
Handler: r,
}
go func() {
if err := srv.ListenAndServe(); err != nil {
log.Println(err)
}
}()
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
log.Println(srv.Shutdown(ctx))
hosts.RemoveHosts(hostArr)
log.Println(hosts.Save())
log.Println("shutting down")
os.Exit(0)
}

func ReverseProxy(w http.ResponseWriter, r *http.Request) {
u, err := url.Parse(proxyUrl)
if err != nil {
log.Println(err)
}
proxy := httputil.NewSingleHostReverseProxy(u)
r.Host = host
proxy.ServeHTTP(w, r)
}

middleware.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88

import (
"bytes"
"encoding/json"
"errors"
"io/ioutil"
"log"
"net/http"
"time"
)

var loginUrl = proxyUrl + "login/sms"

func LoggingHandler(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
t1 := time.Now()
next.ServeHTTP(w, r)
t2 := time.Now()
log.Printf("[%s] %q %v", r.Method, r.URL.String(), t2.Sub(t1))
}
return http.HandlerFunc(fn)
}

func RecoverHandler(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("Recover from panic: %+v", err)
http.Error(w, http.StatusText(500), 500)
}
}()
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}

func AuthHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token, err := getAccessToken()
if err != nil {
log.Fatalln(err)
}
r.Header.Set("X-Auth-AppId", "10003")
r.Header.Set("X-Auth-Token", token)
next.ServeHTTP(w, r)
})
}

func getAccessToken() (string, error) {
params := map[string]string{
"phone": "15555555551",
"verifyCode": "888888",
"codeId": "111111",
}
marshal, _ := json.Marshal(params)
reader := bytes.NewReader(marshal)
request, _ := http.NewRequest("POST", loginUrl, reader)
request.Header.Add("X-Auth-AppId", "10003")
request.Header.Set("content-type", "application/json")
client := &http.Client{}
do, err := client.Do(request)
if err != nil {
return "", err
}
b, err := ioutil.ReadAll(do.Body)
if err != nil {
return "", err
}
_ = do.Body.Close()
result := &LoginResult{}
if err := json.Unmarshal(b, result); err != nil {
return "", err
}
if result.ErrCode != "0" {
return "", errors.New(result.Message)
}
return result.Data.Token, nil
}

type LoginResult struct {
ErrCode string
Message string
Data struct {
Name string
Token string
Phone string
}
}

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package main

import (
"context"
"github.com/gorilla/mux"
"github.com/txn2/txeh"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/signal"
)

var hostArr = []string{"project-01", "project-02"}

const (
host = "api-dev.deveye.cn"
proxyUrl = "https://api-dev.deveye.cn/"
address = "127.0.0.1"
)

func main() {
hosts, err := txeh.NewHostsDefault()
if err != nil {
panic(err)
}
hosts.AddHosts(address, hostArr)
log.Println(hosts.Save())
r := mux.NewRouter()
r.Use(LoggingHandler, AuthHandler, RecoverHandler)
r.PathPrefix("/").HandlerFunc(ReverseProxy)
srv := &http.Server{
Addr: ":80",
Handler: r,
}
go func() {
if err := srv.ListenAndServe(); err != nil {
log.Println(err)
}
}()
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
log.Println(srv.Shutdown(ctx))
hosts.RemoveHosts(hostArr)
log.Println(hosts.Save())
log.Println("shutting down")
os.Exit(0)
}

func ReverseProxy(w http.ResponseWriter, r *http.Request) {
u, err := url.Parse(proxyUrl)
if err != nil {
log.Println(err)
}
proxy := httputil.NewSingleHostReverseProxy(u)
r.Host = host
proxy.ServeHTTP(w, r)
}

middleware.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88

import (
"bytes"
"encoding/json"
"errors"
"io/ioutil"
"log"
"net/http"
"time"
)

var loginUrl = proxyUrl + "login/sms"

func LoggingHandler(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
t1 := time.Now()
next.ServeHTTP(w, r)
t2 := time.Now()
log.Printf("[%s] %q %v", r.Method, r.URL.String(), t2.Sub(t1))
}
return http.HandlerFunc(fn)
}

func RecoverHandler(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("Recover from panic: %+v", err)
http.Error(w, http.StatusText(500), 500)
}
}()
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}

func AuthHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token, err := getAccessToken()
if err != nil {
log.Fatalln(err)
}
r.Header.Set("X-Auth-AppId", "10003")
r.Header.Set("X-Auth-Token", token)
next.ServeHTTP(w, r)
})
}

func getAccessToken() (string, error) {
params := map[string]string{
"phone": "15555555551",
"verifyCode": "888888",
"codeId": "111111",
}
marshal, _ := json.Marshal(params)
reader := bytes.NewReader(marshal)
request, _ := http.NewRequest("POST", loginUrl, reader)
request.Header.Add("X-Auth-AppId", "10003")
request.Header.Set("content-type", "application/json")
client := &http.Client{}
do, err := client.Do(request)
if err != nil {
return "", err
}
b, err := ioutil.ReadAll(do.Body)
if err != nil {
return "", err
}
_ = do.Body.Close()
result := &LoginResult{}
if err := json.Unmarshal(b, result); err != nil {
return "", err
}
if result.ErrCode != "0" {
return "", errors.New(result.Message)
}
return result.Data.Token, nil
}

type LoginResult struct {
ErrCode string
Message string
Data struct {
Name string
Token string
Phone string
}
}

Prometheus快速入门

Prometheus快速入门

一、背景

我们日常开发和运维中,需要监控一些数据,根据这些数据进行分析或者应对策略,例如以下几个维度:

场景                         描述
硬件系统 温度,硬件故障
系统监控 CPU,内存,磁盘,网卡流量,TCP状态,进程数
应用监控 Nginx,Tomcat,PHP,MySQL,Redis 等
日志监控 系统日志,服务日志,访问日志,错误日志
安全监控 WAF,敏感文件监控
API监控 可用性,接口请求,响应时间
业务监控 例如电商网站,每分钟产生多少订单,注册多少用户,多少活跃用户,推广活动效果
流量分析 根据流量获取用户相关信息,例如用户地理位置,某页面访问状况,页面停留时间等

二、Prometheus是什么?

Prometheus 是由 SoundCloud 开发的开源监控报警系统和时序列数据库(TSDB)。

Prometheus由Go语言编写而成,采用Pull方式获取监控信息,并提供了多维度的数据模型和灵活的查询接口。
Prometheus不仅可以通过静态文件配置监控对象,还支持自动发现机制,能通过Kubernetes、Consl、DNS等多种方式动态获取监控对象。
在数据采集方面,借助Go语言的高并发特性,单机Prometheus可以采取数百个节点的监控数据;在数据存储方面,随着本地时序数据库的不断优化,单机Prometheus每秒可以采集一千万个指标,如果需要存储大量的历史监控数据,则还支持远程存储。

三、为什么需要Prometheus?

  • 对于运维人员来说,他们需要监控机器的 CPU、内存、硬盘的使用情况,以此来保证运行在机器上的应用的稳定性。
  • 对于研发人员来说,他们关注某个异常指标的变化情况,从而来保证业务的稳定运行。
  • 对于产品或运营来说,他们更关心产品层面的事情,例如:某个活动参加人数的增长情况,活动积分的发放情况。

例如:运维希望在 CPU 达到 80% 的时候给值班的运维人员发送邮件,产品希望活动积分发放数量超过 10 万的时候发送告警邮件。这些都可以通过 Prometheus 实现。

对于流量不是很大的系统来说,出现几分钟的故障可能造成不了多少损失。但是对于像淘宝、美团、字节跳动这样的巨无霸来说,宕机 1 分钟损失的金额可能就是几百万!

所以弄清楚此时此刻系统的运行是否正常?各项业务指标是否超过阈值?这些问题是每个经验丰富的研发人员所需要关注的事情!

那么如何监控你的系统?如何得知系统目前是正常还是异常?甚至如何预知未来一段时间系统可能出问题?Prometheus 正是这么一套数据监控解决方案。它能让你随时掌控系统的运行状态,快速定位出现问题的位置,快速排除故障。

只要按照 Prometheus 的方式来做,按部就班地学习和部署,我们就可以监控机器的 CPU、内存等资源的使用情况、Java 应用的运行情况以及业务各项指标的实时数据。

而通过 Prometheus 则可以直接部署使用,并且其与 Grafana 配套使用可以呈现出非常多样化的图表配置。对于中小规模的团队来说,可以极大地减少成本,加快研发速度。

而对于个人来讲,掌握 Prometheus 可以增加你当 leader 的竞争力。 毕竟如果一个研发对自己的系统运行状况都不了解,那么他怎么做 leader,怎么带领一个团队往前冲呢?

四、Prometheus Quickly Start

1、实现目标

实现一个Web服务接口请求速率状况的监控功能,当接口请求增长速率超出设定的阈值时,发送钉钉消息通知给研发运维人员。

2、服务搭建

这里使用Docker 搭建 prometheus、alertmanager和dingtalk.

① 创建对应文件目录:

1
2
3
4
mkdir -p ~/prometheus/ && cd ~/prometheus/ && touch prometheus.yml
mkdir -p ~/prometheus/groups/nodegroups/ && cd ~/prometheus/groups/nodegroups/ && touch node.json
mkdir -p ~/prometheus/rules/ && cd ~/prometheus/rules/ && touch http-rate.rules
mkdir -p ~/prometheus/alertmanager && /prometheus/alertmanager && touch alertmanager.yml

② 配置对应文件

prometheus.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
global:
scrape_interval: 15s # By default, scrape targets every 15 seconds.
# Attach these labels to any time series or alerts when communicating with
# external systems (federation, remote storage, Alertmanager).
external_labels:
monitor: 'codelab-monitor'

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
# The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
- job_name: 'prometheus'

# Override the global default and scrape targets from this job every 5 seconds.
scrape_interval: 5s
static_configs:
- targets: ['localhost:9090']

- job_name: 'prometheus-sample'
# 可以在不用重启的情况下热加载配置文件
file_sd_configs:
- files: ['/usr/local/prometheus/groups/nodegroups/*.json']
# Alertmanager configuration
alerting:
alertmanagers:
- static_configs:
- targets:
- 192.168.220.121:9093
rule_files:
- "/usr/local/prometheus/rules/*.rules"

node.json

1
2
3
4
5
6
7
8
9
[
{
"targets": ["192.168.220.121:8080","192.168.220.121:8081"],
"labels": {
"instance": "vm-192.168.220.121",
"service": "node-service"
}
}
]

http-rate.rules

1
2
3
4
5
6
7
8
9
10
groups:
- name: http-rate
rules:
- alert: http-rate
expr: rate(prometheus_sample_hello_world_count[1m]) > 100
for: 15s
labels:
severity: 1
annotations:
summary: "{{ $labels.instance }} HTTP 1min请求增长率超过100%"

alertmanager.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 全局配置项
global:
resolve_timeout: 5m # 处理超时时间,默认为5min

# 定义路由树信息
route:
group_by: [alertname] # 报警分组依据
receiver: ops_notify # 设置默认接收人
group_wait: 30s # 最初即第一次等待多久时间发送一组警报的通知
group_interval: 60s # 在发送新警报前的等待时间
repeat_interval: 1h # 重复发送告警时间。默认1h
routes:

- receiver: ops_notify # 基础告警通知
group_wait: 10s
match_re:
alertname: 实例存活告警|磁盘使用率告警 # 匹配告警规则中的名称发送

# 定义基础告警接收者
receivers:
- name: ops_notify
webhook_configs:
- url: http://192.168.220.121:8060/dingtalk/webhook/send
send_resolved: true # 警报被解决之后是否通知
# 一个inhibition规则是在与另一组匹配器匹配的警报存在的条件下,使匹配一组匹配器的
警报失效的规则。两个警报必须具有一组相同的标签。
inhibit_rules:
- source_match:
severity: 'critical'
target_match:
severity: 'warning'
equal: ['alertname', 'dev', 'instance']

config.yml

1
2
3
4
5
targets:
webhook:
url: https://oapi.dingtalk.com/robot/send?access_token=xxx
# secret for signature
secret: SECxxx

docker 创建服务

1
2
3
4
5
6
7
8
9
10
11
12
13
docker run --name prometheus -d -p 9090:9090 \
-v ~/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml \
-v ~/prometheus/groups/:/usr/local/prometheus/groups/ \
-v ~/prometheus/rules/:/usr/local/prometheus/rules/ \
prom/prometheus:latest

docker run --name alertmanager -d -p 9093:9093 \
-v ~/prometheus/alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml \
prom/alertmanager:latest

docker run --name dingtalk -d -p 8060:8060 \
-v ~/prometheus/dingtalk/config.yml:/etc/prometheus-webhook-dingtalk/config.yml \
timonwong/prometheus-webhook-dingtalk

3、客户端代码

这里使用Go语言编写的一个简易的Web服务,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
"flag"
"fmt"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"log"
"net/http"
)

var port = flag.Int("p", 8080, "-port 8080")

func main() {
flag.Parse()
prometheus.Register(httpCount)
http.Handle("/metrics", promhttp.Handler())
http.HandleFunc("/hello", HelloWorld)
log.Fatalln(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
}

var httpCount = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "prometheus_sample_hello_world_count",
Help: "hello world count!",
},
)

func HelloWorld(w http.ResponseWriter, r *http.Request) {
httpCount.Inc()
w.Write([]byte("Hello World!"))
}

这里我们启动两个节点:

1
2
go run main -p 8080
go run main -p 8081

4、Prometheus Dashboard

打开仪表盘:http://localhost:9090/targets,我们可以看到我们的两个节点已经是up状态了。

下面我们使用ab 工具模拟接口访问,使请求速度超过我们设定的阈值,此时alertmanages会给我们钉钉进行通知。

1
ab -n 1000  -c 5  http://localhost:8080/hello

我可以查看Prometheus控制台,查看消息警告发送情况,此时,我们的钉钉已经收到通知警告:

5、总结

我们通过一个简单的实验,实现了一个接口请求速率监控的例子,实际业务中我们还可以做更多服务监控的功能。也可以结合Grafana进行Dashbords数据报表展示等功能。同时可以监控服务器、JVM、k8s等等,当服务按照我们设置的规则达到阈值时通知到我们,我们可以在第一时间做应对处理,避免造成不必要的损失。

Prometheus快速入门

一、背景

我们日常开发和运维中,需要监控一些数据,根据这些数据进行分析或者应对策略,例如以下几个维度:

场景                         描述
硬件系统 温度,硬件故障
系统监控 CPU,内存,磁盘,网卡流量,TCP状态,进程数
应用监控 Nginx,Tomcat,PHP,MySQL,Redis 等
日志监控 系统日志,服务日志,访问日志,错误日志
安全监控 WAF,敏感文件监控
API监控 可用性,接口请求,响应时间
业务监控 例如电商网站,每分钟产生多少订单,注册多少用户,多少活跃用户,推广活动效果
流量分析 根据流量获取用户相关信息,例如用户地理位置,某页面访问状况,页面停留时间等

二、Prometheus是什么?

Prometheus 是由 SoundCloud 开发的开源监控报警系统和时序列数据库(TSDB)。

Prometheus由Go语言编写而成,采用Pull方式获取监控信息,并提供了多维度的数据模型和灵活的查询接口。
Prometheus不仅可以通过静态文件配置监控对象,还支持自动发现机制,能通过Kubernetes、Consl、DNS等多种方式动态获取监控对象。
在数据采集方面,借助Go语言的高并发特性,单机Prometheus可以采取数百个节点的监控数据;在数据存储方面,随着本地时序数据库的不断优化,单机Prometheus每秒可以采集一千万个指标,如果需要存储大量的历史监控数据,则还支持远程存储。

三、为什么需要Prometheus?

  • 对于运维人员来说,他们需要监控机器的 CPU、内存、硬盘的使用情况,以此来保证运行在机器上的应用的稳定性。
  • 对于研发人员来说,他们关注某个异常指标的变化情况,从而来保证业务的稳定运行。
  • 对于产品或运营来说,他们更关心产品层面的事情,例如:某个活动参加人数的增长情况,活动积分的发放情况。

例如:运维希望在 CPU 达到 80% 的时候给值班的运维人员发送邮件,产品希望活动积分发放数量超过 10 万的时候发送告警邮件。这些都可以通过 Prometheus 实现。

对于流量不是很大的系统来说,出现几分钟的故障可能造成不了多少损失。但是对于像淘宝、美团、字节跳动这样的巨无霸来说,宕机 1 分钟损失的金额可能就是几百万!

所以弄清楚此时此刻系统的运行是否正常?各项业务指标是否超过阈值?这些问题是每个经验丰富的研发人员所需要关注的事情!

那么如何监控你的系统?如何得知系统目前是正常还是异常?甚至如何预知未来一段时间系统可能出问题?Prometheus 正是这么一套数据监控解决方案。它能让你随时掌控系统的运行状态,快速定位出现问题的位置,快速排除故障。

只要按照 Prometheus 的方式来做,按部就班地学习和部署,我们就可以监控机器的 CPU、内存等资源的使用情况、Java 应用的运行情况以及业务各项指标的实时数据。

而通过 Prometheus 则可以直接部署使用,并且其与 Grafana 配套使用可以呈现出非常多样化的图表配置。对于中小规模的团队来说,可以极大地减少成本,加快研发速度。

而对于个人来讲,掌握 Prometheus 可以增加你当 leader 的竞争力。 毕竟如果一个研发对自己的系统运行状况都不了解,那么他怎么做 leader,怎么带领一个团队往前冲呢?

四、Prometheus Quickly Start

1、实现目标

实现一个Web服务接口请求速率状况的监控功能,当接口请求增长速率超出设定的阈值时,发送钉钉消息通知给研发运维人员。

2、服务搭建

这里使用Docker 搭建 prometheus、alertmanager和dingtalk.

① 创建对应文件目录:

1
2
3
4
mkdir -p ~/prometheus/ && cd ~/prometheus/ && touch prometheus.yml
mkdir -p ~/prometheus/groups/nodegroups/ && cd ~/prometheus/groups/nodegroups/ && touch node.json
mkdir -p ~/prometheus/rules/ && cd ~/prometheus/rules/ && touch http-rate.rules
mkdir -p ~/prometheus/alertmanager && /prometheus/alertmanager && touch alertmanager.yml

② 配置对应文件

prometheus.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
global:
scrape_interval: 15s # By default, scrape targets every 15 seconds.
# Attach these labels to any time series or alerts when communicating with
# external systems (federation, remote storage, Alertmanager).
external_labels:
monitor: 'codelab-monitor'

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
# The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
- job_name: 'prometheus'

# Override the global default and scrape targets from this job every 5 seconds.
scrape_interval: 5s
static_configs:
- targets: ['localhost:9090']

- job_name: 'prometheus-sample'
# 可以在不用重启的情况下热加载配置文件
file_sd_configs:
- files: ['/usr/local/prometheus/groups/nodegroups/*.json']
# Alertmanager configuration
alerting:
alertmanagers:
- static_configs:
- targets:
- 192.168.220.121:9093
rule_files:
- "/usr/local/prometheus/rules/*.rules"

node.json

1
2
3
4
5
6
7
8
9
[
{
"targets": ["192.168.220.121:8080","192.168.220.121:8081"],
"labels": {
"instance": "vm-192.168.220.121",
"service": "node-service"
}
}
]

http-rate.rules

1
2
3
4
5
6
7
8
9
10
groups:
- name: http-rate
rules:
- alert: http-rate
expr: rate(prometheus_sample_hello_world_count[1m]) > 100
for: 15s
labels:
severity: 1
annotations:
summary: "{{ $labels.instance }} HTTP 1min请求增长率超过100%"

alertmanager.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 全局配置项
global:
resolve_timeout: 5m # 处理超时时间,默认为5min

# 定义路由树信息
route:
group_by: [alertname] # 报警分组依据
receiver: ops_notify # 设置默认接收人
group_wait: 30s # 最初即第一次等待多久时间发送一组警报的通知
group_interval: 60s # 在发送新警报前的等待时间
repeat_interval: 1h # 重复发送告警时间。默认1h
routes:

- receiver: ops_notify # 基础告警通知
group_wait: 10s
match_re:
alertname: 实例存活告警|磁盘使用率告警 # 匹配告警规则中的名称发送

# 定义基础告警接收者
receivers:
- name: ops_notify
webhook_configs:
- url: http://192.168.220.121:8060/dingtalk/webhook/send
send_resolved: true # 警报被解决之后是否通知
# 一个inhibition规则是在与另一组匹配器匹配的警报存在的条件下,使匹配一组匹配器的
警报失效的规则。两个警报必须具有一组相同的标签。
inhibit_rules:
- source_match:
severity: 'critical'
target_match:
severity: 'warning'
equal: ['alertname', 'dev', 'instance']

config.yml

1
2
3
4
5
targets:
webhook:
url: https://oapi.dingtalk.com/robot/send?access_token=xxx
# secret for signature
secret: SECxxx

docker 创建服务

1
2
3
4
5
6
7
8
9
10
11
12
13
docker run --name prometheus -d -p 9090:9090 \
-v ~/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml \
-v ~/prometheus/groups/:/usr/local/prometheus/groups/ \
-v ~/prometheus/rules/:/usr/local/prometheus/rules/ \
prom/prometheus:latest

docker run --name alertmanager -d -p 9093:9093 \
-v ~/prometheus/alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml \
prom/alertmanager:latest

docker run --name dingtalk -d -p 8060:8060 \
-v ~/prometheus/dingtalk/config.yml:/etc/prometheus-webhook-dingtalk/config.yml \
timonwong/prometheus-webhook-dingtalk

3、客户端代码

这里使用Go语言编写的一个简易的Web服务,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
"flag"
"fmt"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"log"
"net/http"
)

var port = flag.Int("p", 8080, "-port 8080")

func main() {
flag.Parse()
prometheus.Register(httpCount)
http.Handle("/metrics", promhttp.Handler())
http.HandleFunc("/hello", HelloWorld)
log.Fatalln(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
}

var httpCount = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "prometheus_sample_hello_world_count",
Help: "hello world count!",
},
)

func HelloWorld(w http.ResponseWriter, r *http.Request) {
httpCount.Inc()
w.Write([]byte("Hello World!"))
}

这里我们启动两个节点:

1
2
go run main -p 8080
go run main -p 8081

4、Prometheus Dashboard

打开仪表盘:http://localhost:9090/targets,我们可以看到我们的两个节点已经是up状态了。

下面我们使用ab 工具模拟接口访问,使请求速度超过我们设定的阈值,此时alertmanages会给我们钉钉进行通知。

1
ab -n 1000  -c 5  http://localhost:8080/hello

我可以查看Prometheus控制台,查看消息警告发送情况,此时,我们的钉钉已经收到通知警告:

5、总结

我们通过一个简单的实验,实现了一个接口请求速率监控的例子,实际业务中我们还可以做更多服务监控的功能。也可以结合Grafana进行Dashbords数据报表展示等功能。同时可以监控服务器、JVM、k8s等等,当服务按照我们设置的规则达到阈值时通知到我们,我们可以在第一时间做应对处理,避免造成不必要的损失。

心中需破除的三种贼

坐中静,破焦虑之贼——可以用冥想打坐等方法让自己回到当下,人之所以会焦虑是基于过去的经验对未来产生的恐惧,能够活在当下的人不会焦虑。把每一天每一刻过好,人生自然就会好。
舍中得,破欲望之贼——人的痛苦多来自于欲望,即为贪婪和占有,也是一种匮乏的表现,如果能够学会舍,则内心越来越丰盛。没钱的时候把勤舍出去即为天道酬勤;有钱的时候把钱舍出去人就来了,是为轻财聚人;有人的时候,把爱舍出去,成功便有了;事业有成的时候,把智慧舍出去,喜悦就来了,这就是德行天下。
事上练,破犹豫之贼——这里讲的大概是王阳明的知行合一。光懂了很多道理是过不好这一生的,唯有践行其中的道理才能真正成事,只要想到就去做,犹豫纠结之贼就能破,达到知行合一的最高境界,则万事可成。

作者:数中有术作者
链接:https://www.zhihu.com/question/448566584/answer/1898949098
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

坐中静,破焦虑之贼——可以用冥想打坐等方法让自己回到当下,人之所以会焦虑是基于过去的经验对未来产生的恐惧,能够活在当下的人不会焦虑。把每一天每一刻过好,人生自然就会好。
舍中得,破欲望之贼——人的痛苦多来自于欲望,即为贪婪和占有,也是一种匮乏的表现,如果能够学会舍,则内心越来越丰盛。没钱的时候把勤舍出去即为天道酬勤;有钱的时候把钱舍出去人就来了,是为轻财聚人;有人的时候,把爱舍出去,成功便有了;事业有成的时候,把智慧舍出去,喜悦就来了,这就是德行天下。
事上练,破犹豫之贼——这里讲的大概是王阳明的知行合一。光懂了很多道理是过不好这一生的,唯有践行其中的道理才能真正成事,只要想到就去做,犹豫纠结之贼就能破,达到知行合一的最高境界,则万事可成。

作者:数中有术作者
链接:https://www.zhihu.com/question/448566584/answer/1898949098
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

10x程序员总结

以终为始

遇到问题不妨倒着去思考

任务分解

  • 埃隆马斯克SpaceX 的计划,本质上就是把一件很困难实现的事情去做任务分解,使每件任务可执行化。
1
2
3
4
如何降低火箭发射成本
1. 造一个能容纳100人的火箭
2. 实现火箭回收
3. ....
  • 大师程序员都是懂的去做任务分解,分解的力度是可执行

  • 多写单元测试,代码小步提交