前言

关于索引,大家都知道能提高查询效率,是能高效获取数据的数据结构。但是为什么MySQL选择使用B+树?关于这个问题很多人都有自己的理解,但都回答得不够完整,也有人只是讲了B+树和B树的区别,并没有真正回答MySQL为什么选择B+树这个问题。想要知道答案,我们接下来会深入分析各种场景下不同数据结构的优缺点。

首先需要澄清的一点是,MySQL 跟 B+ 树没有直接的关系,真正与 B+ 树有关系的是 MySQL 的默认存储引擎 InnoDB,MySQL 中存储引擎的主要作用是负责数据的存储和提取,除了 InnoDB 之外,MySQL 中也支持 MyISAM 作为表的底层存储引擎。

正文

为什么 MySQL 默认的存储引擎 InnoDB 会使用 B+ 树来存储数据,相信对 MySQL 稍微有些了解的人都知道,无论是表中的数据(主键索引)还是非主键索引最终都会使用 B+ 树来存储数据,其中前者在表中会以 <id, row> 的方式存储,而后者会以 <index, id> 的方式进行存储,这其实也比较好理解:

  • 在主键索引中,id 是主键,我们能够通过 id 找到该行的全部列;
  • 在非主键索引中,索引中的几个列构成了键,我们能够通过索引中的列找到 id,如果有需要的话,可以再通过 id 找到当前数据行的全部内容;

在具体分析 InnoDB 使用 B+ 树背后的原因之前,我们需要为 B+ 树找几个假想敌,因为如果我们只有一个选择,那么选择 B+ 树也并不值得讨论,找到的两个假想敌就是 B 树和哈希,相信这也是很多人会在面试中真实遇到的问题,我们就以这两种数据结构为例,分析比较 B+ 树的优点。

读写性能

只访问或者修改一条数据的SQL时

1
select * from user where id=1;
  • 使用B+树作为底层的数据结构,时间复杂度是O(log n);
  • 使用哈希作为底层的数据结构,时间复杂度是O(1);

看起来使用哈希非常美好,但是我们如果使用如下所示的SQL时

1
2
select * from user where id>10 order by create_time desc;
select * from user where id>10 and crete_time>1646636628;

此场景下,如果使用哈希作为底层的数据结构,可能就无法快速处理了,因为它对于范围查询或者排序的性能非常差,只能进行全表扫描并依次判断是否符合结果。
全表扫描对于MySQL是无法接受的糟糕结果,这其实意味着我们选择的数据结构没有任何效果。

哈希与B+树

哈希与B+树

B 树和 B+ 树在数据结构上其实有一些类似,它们都可以按照某些顺序对索引中的内容进行遍历,对于排序和范围查询等操作,B 树和 B+ 树相比于哈希会带来更好的性能,当然如果索引建立不够好或者 SQL 查询非常复杂,依然会导致全表扫描。

数据加载

使用哈希作为底层的数据结构无法高效的处理常见的范围查询和排序等操作,但是使用B+树或者B树作为底层的数据结构能非常高效的的应对。既然这样,为什么没有选择B树?其实原因很简单:
B 树能够在非叶子节点中存储数据,但是这也导致在查询连续数据时可能会带来更多的随机 I/O,而 B+ 树的所有叶子节点可以通过指针相互连接,能够减少顺序遍历时产生的额外随机 I/O;

查询数据

查询数据

画外音:“这里的随机I/O指MySQL查询数据时,CPU发现当前数据位于磁盘而不是内存中,这时会触发I/O操作将数据以页为维度加载到内存中,然而数据从磁盘读取到内存中的代价是巨大的。”

b树与b+树

b树与b+树

另外是由于数据结构的不同,查找范围数据的方式不同。B树非叶子节点存储数据,所有节点都可能包含数据,我们总要从根节点向下遍历子树找到满足条件的数据行,这个特点带来了更多的随机I/O,也是B树最大的性能问题。

B+树中不存在这个问题,通过一个 B+ 树最左侧的叶子节点,我们可以像链表一样遍历整个树中的全部数据,我们也可以引入双向链表保证倒序遍历时的性能。

总结

我们在这里重新回顾一下 MySQL 默认的存储引擎选择 B+ 树而不是哈希或者 B 树的原因:

  • 哈希虽然能够提供 O(1) 的单数据行操作性能,但是对于范围查询和排序却无法很好地支持,最终导致全表扫描;

  • B 树能够在非叶子节点中存储数据,但是这也导致在查询连续数据时可能会带来更多的随机 I/O,而 B+ 树的所有叶子节点可以通过指针相互连接,能够减少顺序遍历时产生的额外随机 I/O;

参考资料

What is the difference between Mysql InnoDB B+ tree index and hash index? Why does MongoDB use B-tree[https://medium.com/@mena.meseha/what-is-the-difference-between-mysql-innodb-b-tree-index-and-hash-index-ed8f2ce66d69]

B-Tree vs Hash Table[https://stackoverflow.com/questions/7306316/b-tree-vs-hash-table]

前言

Redis集群部署方案都是基于主从架构演变而来的,Redis主从架构是常见的一种数据冗余和热备份技术,对于大型数据应用可以提供非常大的便利,但也有其缺点,比如数据丢失。

由于Redis主从复制默认是异步的,如果主服务器在数据还未来得及复制到从服务器时宕机,可能会发生数据丢失。

本文讨论的主从架构是常见哨兵模式,普通主从模式,主从节点为手动切换,是否丢失数据受人为因素影响。

正文

主从架构下,到底发生了什么?导致的数据丢失。常见的情况有两种

  • 异步复制过程中,master节点宕机;

  • 集群产生脑裂。

异步复制过程中,master节点宕机

Redis 的异步复制(asynchronous replication)过程中,主节点(master)对数据的修改操作会首先在自身完成,然后异步地发送到各从节点(slaves)进行复制。也就是说,主节点上有一部分的修改可能还没有来得及同步到各从节点,而此时如果主节点发生宕机或者其他错误导致服务停止,那么这部分还未来得及复制的数据就会丢失。

详细解释一下,redis 的主从复制基本流程包括:

  • 从节点连接到主节点,发送 SYNC 命令。

  • 主节点在收到 SYNC 命令后执行 BGSAVE 命令,在后台生成 RDB 文件,并使用一个缓冲区记录此后执行的所有写命令(这就是所说的异步复制)。

  • 主节点 BGSAVE 命令执行完毕后,将 RDB 文件发送给从节点,从节点接收并载入这个 RDB 文件,然后开始接收主节点发送过来的缓冲区中的写命令。

在这个过程中,如果主节点宕机,尤其是在步骤3的过程中,那么缓冲区中的写命令可能还没来得及发送给从节点,就会导致数据丢失。这也是为什么主从同步方式是强调数据冗余和服务高可用,并不能保证数据的强一致性。

疑问:master节点开启持久化能解决这个问题吗?

集群产生脑裂

简单来说,集群脑裂是指一个集群中有多个主节点,像一个人有两个大脑,到底听谁的呢?

例如,由于网络原因,master与slave节点之间断开了联系,哨兵检测后认为主节点故障,重新选举从节点为主节点,但主节点可能并没有发生故障。此时客户端依然在旧的主节点上写数据,而新的主节点中没有数据,在发现这个问题之后,旧的主节点会被降为slave,并且开始同步新的master数据,那么之前的写入旧的主节点的数据被刷新掉,大量数据丢失。

脑裂

如何解决“脑裂”导致的数据丢失,关键点在于t3时刻阻止客户端往主库写入数据,这样t5时刻主从切换时,主库中并没有新数据。

Redis 已经提供了两个配置项来限制主库的请求处理,分别是 min-slaves-to-write 和 min-slaves-max-lag。

  • min-slaves-to-write:这个配置项设置了主库能进行数据同步的最少从库数量;

  • min-slaves-max-lag:这个配置项设置了主从库间进行数据复制时,从库给主库发送 ACK 消息的最大延迟(以秒为单位)。

有了这两个配置项后,我们就可以轻松地应对脑裂问题了。具体咋做呢?我们可以把 min-slaves-to-write 和 min-slaves-max-lag 这两个配置项搭配起来使用,分别给它们设置一定的阈值,假设为 N 和 T。

这两个配置项组合后的要求是,主库连接的从库中至少有 N 个从库,和主库进行数据复制时的 ACK 消息延迟不能超过 T 秒,否则,主库就不会再接收客户端的请求了。

即使原主库是假故障,它在假故障期间也无法响应哨兵心跳,也不能和从库进行同步,自然也就无法和从库进行 ACK 确认了。这样一来,min-slaves-to-write 和 min-slaves-max-lag 的组合要求就无法得到满足,原主库就会被限制接收客户端请求,客户端也就不能在原主库中写入新数据了。也就达到了“t3时刻阻止客户端往主库写入数据”的目的。

总结

哨兵模式下的主从架构会发生数据丢失,两种情况:

  • 异步复制过程中,master节点宕机;

  • 集群产生脑裂。

可以通过限制主从节点切换期间旧主节点写入新数据,两个参数控制:

  • min-slaves-to-write:这个配置项设置了主库能进行数据同步的最少从库数量;

  • min-slaves-max-lag:这个配置项设置了主从库间进行数据复制时,从库给主库发送 ACK 消息的最大延迟(以秒为单位)。

参考资料

脑裂:一次奇怪的数据丢失[https://time.geekbang.org/column/article/303568]

go的并发模型围绕两个基本概念:goroutine和channel.
goroutine是由go运行时管理的轻量级线程,而channel是goroutine之间通信的主要手段。
在文中,我们将深入研究go的并发性,重点关注它提供的特性及如何利用他们创建高性能的程序。

什么是并发?

并发是指程序或系统同时运行多个任务的能力,允许它们在重叠的时间范围内独立执行。
在程序需要同时处理多个任务的情况下,并发性非常重要。例如处理多个客户端的请求,并发处理数据,消息队列的消费等。
在并发编程中,通常使用线程、轻量级线程(如goroutine)或者其他并发执行单元来执行任务。这些并发执行单元可以独立执行其操作,并在需要时相互通信。

什么是goroutine?

go中goroutine是轻量级线程,可以并发执行代码,是go语言并发模型中的基本模块,并在高效且可扩展性的并发编程中扮演着重要角色。
goroutine如此特别的原因在于其轻量级特性,go中的goroutine由go在运行时管理,不会占用太多内存。
goroutine初始化时申请的资源很少,它们可以动态的增加或缩小堆栈,这意味着单个程序要中你可以创建数千甚至数百万个goroutine,而不用担心内存开销。
下面是一段示例代码,演示了如何使用goroutine在程序中进行并发执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package utils

import (
"fmt"
"time"
)

func printMessage(message string) {
for i := 0; i < 5; i++ {
fmt.Println(message)
time.Sleep(time.Millisecond * 500)
}
}

func main() {
go printMessage("Hello") // Create a goroutine to print "Hello"
printMessage("World") // Execute printMessage("World") in the main goroutine

// Sleep for a while to allow the goroutine to execute
time.Sleep(time.Second * 3)
}

在此示例中,我们的printMessage函数多次打印给定消息,每次打印之间有很小的延迟。我们使用 go 关键字创建一个 goroutine 来并发执行该printMessage(“Hello”)函数。同时,在主 goroutine 中,我们执行该printMessage(“World”)函数。

结果,程序交错打印消息“Hello”和“World”,表示并发执行。用于打印“Hello”的 goroutine 与主 goroutine 的执行同时运行,从而允许同时打印两条消息。

通过利用goroutine,我们可以实现并发执行,而不会阻塞其他任务的进度。Goroutine 的轻量级和并发特性使我们能够同时执行多个操作,从而使我们的程序更加高效和响应迅速。

请注意,在这个简单的示例中,我们通过使用 引入睡眠周期来允许 goroutine 执行time.Sleep。在现实场景中,您通常会使用通道或等待组等同步机制来协调和同步 goroutine 的执行。

goroutine的好处?

Goroutines 在高效资源利用、非阻塞并发和可扩展性方面提供了一些令人惊叹的好处:

  • 高效的资源利用:与传统线程相比,Goroutines 的内存效率更高。它们的堆栈大小较小,这意味着它们占用的内存较少。这种效率非常方便,尤其是当您想要创建大量并发单元时。使用 goroutine,您可以构建高度并发的应用程序,这些应用程序可以优雅地扩展,而不会占用过多的资源。

  • 非阻塞并发:Goroutines 可以并发执行而不会造成阻塞。当一个 Goroutine 遇到阻塞操作时,比如等待 I/O 或时间延迟,Go 调度器会巧妙地暂停该 Goroutine 并切换到另一个就绪的 Goroutine。这种魔力有助于优化 CPU 时间使用并防止不必要的等待,从而产生超级响应的应用程序。

  • 可扩展性:Goroutines 是轻量级的,使得扩展应用程序变得轻而易举。您可以轻松创建大量并发单元来处理多个任务或处理大量数据。这种可扩展性对于需要大量并行性的情况特别方便,例如管理网络请求或处理大量数据集。

凭借这些功能,goroutine 可以轻松开发高效、响应灵敏且可扩展的应用程序。

什么是channel?

它们在确保这些 goroutine 协同工作并保持同步、防止任何混乱或冲突方面发挥着至关重要的作用。

想象一下,您有一个 goroutine 团队,每个 goroutine 同时处理不同的任务。如果没有通道,这些 goroutine 可能会发生冲突并造成各种麻烦。但有了通道,你就可以通过明确定义的方式让 goroutine 进行通信和共享信息。

创建通道就像建立通信线路一样。您决定什么类型的数据可以流过它,然后 goroutine 可以使用 <- 运算符发送或接收值。这就像同事之间来回传递纸条一样。

以下是说明通道如何工作的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
"time"
)

func sendMessage(msg string, ch chan<- string) {
time.Sleep(time.Second) // Pretend to do some work
ch <- msg // Sending the message through the channel
}

func main() {
messageChannel := make(chan string, 1) // Creating a channel for string messages

go sendMessage("Hello", messageChannel) // Sending "Hello" through the channel in a goroutine

receivedMsg := <-messageChannel // Receiving the message from the channel

fmt.Println("Received message:", receivedMsg)
}

在这个例子中,我们有一个sendMessage函数,它代表一个通过通道发送消息的 goroutine。我们make(chan string, 1)在主函数中使用创建一个通道,专门用于字符串消息。运算<-符用于从通道发送消息 ( ch <- msg) 和接收消息 ( )。receivedMsg := <-messageChannel

通道确保 goroutine 保持同步。当一个 goroutine 通过通道发送一个值时,它会等待,直到另一个 goroutine 接收到该值。如果channel中一直未有数据写入的话,另一个goroutine会等待数据可用。这样,两个 goroutine 就得到了适当的协调,并且我们避免了任何混淆或误解。

channel的好处?

Go 中的通道在 goroutine 之间的通信和协调方面带来了一些很酷的好处:

  • 顺畅的通信:通道为 goroutine 提供了一种安全且有组织的方式来相互通信和共享数据。他们确保 goroutine 可以在它们之间传递信息,而不会遇到任何混乱的数据冲突或同步问题。这就像拥有一条用于无缝沟通和团队合作的清晰管道。
  • 同步:通道充当 goroutine 同步和协同工作的便捷工具。它们允许 goroutine 互相发送信号、等待数据可用或推迟到特定事件发生。这种同步能力确保每个人都保持在同一页面上,避免冲突,并保持良好协调的操作舞蹈。
  • 不再等待:通道支持阻塞操作,这意味着 goroutine 可以暂停,直到它通过通道接收或发送数据。如果一个 Goroutine 正在等待数据,Go 调度程序会智能地切换到其他就绪的 Goroutine,从而保持忙碌和响应。
  • 缓冲:通道可以配备缓冲区,允许它们在 goroutine 获取多个值之前保存它们。这种缓冲能力有助于解耦数据生成和消耗的速度,防止不必要的故障并提高整体性能。这就像有一个数据临时存储区域,可以在需要时随时取用。
  • 多路复用魔法:Go 中的 select 语句增加了额外的变化。它允许 goroutine 同时处理多个通道,执行非阻塞操作并选择准备通信的通道。这就像有多条电话线并接听正在响铃的电话线。这种多路复用魔法简化了多个通道的处理,使复杂的通信模式看起来非常简单。

通过在 Go 中使用通道,可以保证 goroutine 之间顺畅、协调的通信。通道保持数据流动、协程同步,并且整个并发方像一台运转良好的机器一样运行。这一切都是为了促进团队合作、避免冲突并释放并发应用程序的全部潜力。

结论

总而言之,由 goroutine 和通道驱动的 Go 并发模型为并发编程提供了高效且可扩展的解决方案。goroutine 提供了一个轻量级的并发执行环境,允许任务同时执行,而不会消耗过多的资源。

rdbSaveBackGround函数:将内存中的数据以 RDB 格式保存到磁盘中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
pid_t childpid;

if (hasActiveChildProcess()) return C_ERR;
...

if ((childpid = redisFork()) == 0) {
int retval;

/* Child */
redisSetProcTitle("redis-rdb-bgsave");
retval = rdbSave(filename,rsi);
if (retval == C_OK) {
sendChildCOWInfo(CHILD_INFO_TYPE_RDB, "RDB");
}
exitFromChild((retval == C_OK) ? 0 : 1);
} else {
/* Parent */
...
}
...
}

函数中在快照前会先通过redisFork()创建子进程

  • 为什么 fork 之后的子进程能够获取父进程内存中的数据?

  • fork 函数是否会带来额外的性能开销,这些开销我们怎么样才可以避免?

设计
  • 通过 fork 生成的父子进程会共享包括内存空间在内的资源;

  • fork 函数并不会带来明显的性能开销,尤其是对内存进行大量的拷贝,它能通过写时拷贝将拷贝内存这一工作推迟到真正需要的时候。

Redis 实现后台快照的方式非常巧妙,通过操作系统提供的 fork写时拷贝的特性轻而易举的就实现了这个功能,从这里我们就能看出作者对于操作系统知识的掌握还是非常扎实的,大多人在面对类似的场景时,想到的方法可能就是手动实现类似写时拷贝的特性,然而这不仅增加了工作量,还增加了程序出现问题的可能性。

到这里,我们简单总结一下 Redis 为什么在使用 RDB快照时会通过子进程的方式进行实现:

  • 通过 fork 创建的子进程能够获得和父进程完全相同的内存空间,父进程对内存的修改对于子进程是不可见的,两者不会相互影响;

  • 通过 fork 创建子进程时不会立刻触发大量内存的拷贝,内存在被修改时会以页为单位进行拷贝,这也就避免了大量拷贝内存而带来的性能问题;

上述两个原因中,一个为子进程访问父进程提供了支撑,另一个为减少额外开销做了支持,这两者缺一不可,共同成为了 Redis 使用子进程实现快照持久化的原因。到最后,我们还是来看一些比较开放的相关问题,有兴趣的读者可以仔细思考一下下面的问题:

  • Nginx 的主进程会在运行时 fork 一组子进程,这些子进程可以分别处理请求,还有哪些服务会使用这一特性?

  • 写时拷贝其实是一个比较常见的机制,在 Redis 之外还有哪里会用到它?

为什么Redis主从模式能保持数据一致。想要知道答案,我们得深入分析Redis实例之间如何进行数据同步。

概述

在具体分析今天的问题之前,我们需要了解 Redis 具有高可靠性,又是什么意思呢?其实,这里有两层含义:一是数据尽量少丢失,二是服务尽量少中断。AOFRDB 保证了前者,而对于后者,Redis 的做法就是增加副本冗余量,将一份数据同时保存在多个实例上。即使有一个实例出现了故障,需要过一段时间才能恢复,其他实例也可以对外提供服务,不会影响业务使用。多实例保存同一份数据,听起来好像很不错,但是,我们必须要考虑一个问题:这么多副本,它们之间的数据如何保持一致呢?数据读写操作可以发给所有的实例吗?实际上,Redis 提供了主从库模式,以保证数据副本的一致,主从库之间采用的是读写分离的方式。

1)读操作:主库、从库都可以接收;

2)写操作:首先到主库执行,然后,主库将写操作同步给从库。

到了这里我们发现几个问题:

1)主从库为什么要采用读写分离的方式;

2)主库如何把数据同步给从库;

3)主从库之前网络断了怎么办;

不过既然Redis具有高可用性,说明这些问题都已经有了答案。

设计

主从库为什么要采用读写分离的方式

你可以设想一下,如果不管是主库还是从库,都能接收客户端的写操作,那么,一个直接的问题就是:如果客户端对同一个数据(例如 k1)前后修改了三次,每一次的修改请求都发送到不同的实例上,在不同的实例上执行,那么,这个数据在这三个实例上的副本就不一致了(分别是 v1、v2 和 v3)。在读取这个数据的时候,就可能读取到旧的值。
如果我们非要保持这个数据在三个实例上一致,就要涉及到加锁、实例间协商是否完成修改等一系列操作,但这会带来巨额的开销,当然是不太能接受的。

主库如何把数据同步给从库

当我们启动多个 Redis 实例的时候,它们相互之间就可以通过 replicaof(Redis 5.0 之前使用 slaveof)命令形成主库和从库的关系,之后会按照三个阶段完成数据的第一次同步。

**数据同步 **
数据同步

第一阶段

建立连接,协商同步,psync(runId=?,offest=-1),此时主库会调用FULLRESYNC进行同步。

第二阶段

主库执行 bgsave 命令,生成 RDB 快照文件,接着将文件发给从库。从库接收到 RDB 文件后,会先清空当前数据库,然后加载 RDB 文件。这是因为从库在通过 replicaof 命令开始和主库同步前,可能保存了其他数据。为了避免之前数据的影响,从库需要先把当前数据库清空。可能会有人质疑,主库生成RDB快照文件,不会阻塞主库的读写操作吗?会不会有额外的性能开销?那这些问题我们今天就不展开讨论了,感兴趣的同学可以读下源码。
rdbSaveBackground就是用来处理在后台将数据保存到磁盘上的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
pid_t childpid;

if (hasActiveChildProcess()) return C_ERR;
...

if ((childpid = redisFork()) == 0) {
int retval;

/* Child */
redisSetProcTitle("redis-rdb-bgsave");
retval = rdbSave(filename,rsi);
if (retval == C_OK) {
sendChildCOWInfo(CHILD_INFO_TYPE_RDB, "RDB");
}
exitFromChild((retval == C_OK) ? 0 : 1);
} else {
/* Parent */
...
}
...
}

第三阶段

repl_backlog_buffer

最后,也就是第三个阶段,主库会把第二阶段执行过程中新收到的写命令,再发送给从库。具体的操作是,当主库完成 RDB 文件发送后,就会把此时 replication_buffer 中的修改操作发给从库,从库再重新执行这些操作。这样一来,主从库就实现同步了。

重连恢复
主从库如果断开连接了,下次建立连接时会通过repl_backlog_buffer环形缓冲区进行增量复制,psync(runId=1,offest=100),offest=100用于标记从库在repl_backlog_buffer中的位置。

总结

为什么Redis主从模式能保持数据一致?

  • 采用读写分离,避免加锁、实例间协商是否完成修改等操作,减少不必要的性能损耗;

  • 主从实例间通过RDB快照进行数据同步,同步期间主库的写操作额外记录一份到replication_buffer中,同步完成时,发送给从库,从库再重新执行这些操作。

  • 后续的数据同步通过repl_backlog_buffer来标记主从实例环形缓冲区中的位置,从库执行这些操作。

在 Web 开发中,Nginx(”engine x”)是一个广泛使用的高性能 Web 服务器和反向代理服务器。它可以用于负载均衡、缓存静态资源、HTTPS 配置等,本文将介绍如何配置 Nginx 反向代理。

安装 Nginx

首先,我们需要安装 Nginx。具体步骤因操作系统而异,可参考 Nginx 官方文档中的指南。

编写 Nginx 配置文件

接下来,我们需要编写一个反向代理配置文件。通常,这个文件位于 /etc/nginx/conf.d/ 目录下,并以 .conf 扩展名结尾。以下是一个简单的示例配置文件:

1
2
3
4
5
6
7
8
9
10
11
server {
listen 80;
server_name example.com;

location / {
proxy_pass http://localhost:3000;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
}
}

上述示例配置文件中,listen 80 设置了监听的端口为 80,server_name 指定了要代理的域名或 IP 地址。

location / 表示匹配所有请求,proxy_pass http://localhost:3000 则将请求转发到本地端口为 3000 的服务器。proxy_set_header 设置了请求头信息,包括客户端的真实 IP 地址和转发的域名等。

测试 Nginx 反向代理

配置完成后,我们可以使用以下命令重启 Nginx 并测试反向代理是否有效:

1
2
sudo systemctl restart nginx
curl -I example.com

其中,curl -I 命令用于检查 HTTP 响应头,example.com 替换为您要代理的域名或 IP 地址。

如果一切正常,则会看到来自反向代理服务器的响应头信息。如果出现错误,请检查您的配置文件中是否存在语法错误,并确认本地服务器是否正在运行。

总之,Nginx 反向代理是一种通用且强大的方式来缓解 Web 应用程序的压力。通过正确配置 Nginx 反向代理,您可以更好地管理流量并提高性能。

背景

在 Go 语言中,协程(Goroutine)是一种轻量级线程,可以实现并发执行。但是,在编写协程代码时,可能会遇到 panic 错误,并影响应用程序的性能和稳定性。那么,该如何处理协程 panic 呢?

首先,需要了解协程 panic 的原因。通常,当一个协程出现错误并无法恢复时,会引发 panic。这会导致程序崩溃或运行速度变慢。为了处理协程 panic,我们可以使用以下几种方法:

措施
  1. 使用defer函数:在协程中使用defer函数可以保证在函数退出之前执行一些清理操作,比如释放资源等。这可以减轻panic对程序的影响,同时也有助于排查问题。

  2. 使用recover函数:recover函数可以捕获panic并返回它的值,这样就可以在程序中进行处理。在协程中使用recover可以避免panic将程序终止,而是通过错误处理机制来进行处理。

  3. 限制协程数量:如果程序中存在大量的协程,而且它们都可能会发生panic,那么就可能导致程序的崩溃。为了避免这种情况,可以限制协程的数量,从而减少程序崩溃的可能性。

  4. 使用监控工具:在程序中使用监控工具可以及时发现协程panic的情况,并进行处理。例如,可以使用类似于prometheus之类的监控工具来监控程序的运行情况,并在出现问题时发送警报。

总之,处理协程panic的关键是及时发现它,并采取措施来降低影响。

示例

以下是一个示例代码,演示如何在协程中使用defer和recover来处理panic,以及如何使用sync.WaitGroup来限制协程数量:

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
package main

import (
"fmt"
"sync"
)

func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer func() {
if r := recover(); r != nil {
fmt.Printf("goroutine %d panic: %v\n", id, r)
}
wg.Done()
}()
// do something
if id == 5 {
panic("oh no!")
}
fmt.Printf("goroutine %d completed\n", id)
}(i)
}
wg.Wait()
fmt.Println("all goroutines completed")
}

在这个示例代码中,我们启动了10个协程,并在其中一个协程中使用了panic。在每个协程中,我们使用defer函数来捕获panic并进行处理。我们还使用了sync.WaitGroup来限制协程数量,并在所有协程完成后输出一条消息。

背景

平时工作中遇到线上故障时,往往需要对时间段内的日志进行清洗,得到特定字段值。最后执行修复脚本,从而进行线上故障修复,所以分享几个高效的vim命令,能让此过程更平滑。

进入底线命令模式

按:,进入底线命令模式

替换命令
1
2
3
4
5
:%s/a/b/g //将整个文件的的所有a替换成b
:%s/a/b/ //将每行的第一个a替换成b
:%!grep a //只留下含有a的行
:%!grep -v a //只去掉含有a的行

正则表达式

\d 数字

\D 排除数子

\{n} 出现n次

\{n,m} 出现n-m次

例子
  • 日志
1
2
3
xxxxcredit_flow_1234xxxxx
xxxxcredit_flow_2134xxxxx
xxxxcredit_flow_3124xxxxx
  • 目的

只留含有credit_flow_的行

替换关键字credit_flow_后的数字

  • 命令
    1
    2
    3
    4
    #只留下含有关键字的行
    %!grep credit_flow_
    #将整个文件的的所有符合规则的字符替换成目标字符
    %s/credit_flow_\d\{4}/credit_flow_0000/g