March 29, 2020
redis主从复制是高可用方案中的一部分,那主从复制是如何进行的?又是如何实现的?怎么支撑了redis的高可用性?在主从模式下Master和Slave节点分别做了哪些事情?
redis高可用方案是什么?
#
我理解的redis高可用的特点有:
- 高QPS,主从 => 读写分离
- 高容量,集群分片 => 高容量
- 故障转移,sentinel => 故障转移
- 故障恢复,数据持久 => 故障恢复 ~ 这里我简单的理解(RDB + AOF)= 故障恢复
主从复制
#
redis 主从复制有两个版本:旧版(Ver2.8-),新版(Ver2.8+,增加PSYNC命令来解决旧版中的问题)
讨论复制时都需要考虑两种场景:
- 场景1:从节点刚刚上线,需要去同步主节点时,这部分可以理解为 全量复制。
- 场景2:从节点掉线,恢复上线后需要同步数据,使自己和主节点达到一致状态。这部分在旧版复制里等价于全量复制,在新版里可以理解为增量复制。
当然你肯定会想到如果主节点掉线,这时候会怎么样?这个场景当然也在redis高可用方案中,之时不是本文的重点,属于Sentinel机制的内容了。
旧版主从复制
#
前文说过了,旧版主从复制只有全量复制用于应付上述两个场景,因此下面的流程也只有一份:
- 从服务器向主服务器发送sync命令。
- 主服务器在收到sync命令之后,调用bgsave命令生成最新的rdb文件,将这个文件同步给从服务器,这样从服务器载入这个rdb文件之后,状态就会和主服务器执行bgsave命令时候的一致。
- 主服务器将保存在命令缓冲区中的写命令同步给从服务器,从服务器执行这些命令,这样从服务器的状态就跟主服务器当前状态一致了。
如果你不知道redis中还有个缓冲区的话,建议系统的了解下redis中缓冲区的设计。这里缓冲区特指命令缓冲区,后面还会讲到复制缓冲区。
但是这样的实现在 场景2 下的缺点很明显:如果说从节点断线后迅速上线,这段时间内的产生的写命令很少,却要全量复制主库的数据,传输了大量重复数据。
SYNC命令产生的消耗:
1. 主节点生成RDB,需要消耗大量的CPU,内存和磁盘IO
2. 网络传输大量字节数据,需要消耗主从服务器的网络资源
3. 从节点需要从RDB文件恢复,会造成阻塞无法接受客户端请求
优点就是:简单暴力。个人看来在redis架构中不合适的用法,不代表说实际场景中也一定不合适,简单暴力也是一个很大的优点。
新版主从复制
#
新版的主从复制跟旧版的区别就在于:对场景2的优化。
场景2的缺点上文已经提到过了,那么优化的方向就是**“尽量不使用全量复制;增加增量复制(PSYNC)的功能”**。为此还要解决下列问题:
- 如果某个从节点断线了,重新上线该从节点如何知道自己是否应该全量还是增量复制呢?
- 该从节点断线恢复后,又怎么知道自己缺失了哪些数据呢?
- 主节点又如何补偿该从节点在断线期间丢失的那部分数据呢?旧版的复制除了RDB,还有从命令缓冲区中的写命令来保持数据一致。
为此新版中使用了以下概念:
运行ID - runid
#
每个redis服务器都有其runid,runid由服务器在启动时自动生成,主服务器会将自己的runid发送给从服务器,而从服务器会将主服务器的runid保存起来。从服务器redis断线重连之后进行同步时,就是根据runid来判断同步的进度:
- 如果前后两次主服务器runid一致,则认为这一次断线重连还是之前复制的主服务器,主服务器可以继续尝试部分同步操作。
- 如果前后两次主服务器runid不相同,则全同步流程。
复制偏移量 - offset
#
主从节点,分别会维护一个复制偏移量:
主服务器每次向从服务器同步了N字节数据之后,将修改自己的复制偏移量+N。从服务器每次从主服务器同步了N字节数据之后,将修改自己的复制偏移量+N。通过对比主从节点的偏移量很容易就可以发现,主从节点是否处于一致状态。
...
January 8, 2020
背景
#
生产环境数据库不允许直接访问,但是又经常有需要直接操作数据库的需求😂。先不说合不合理,背景就是这个背景,因此只能通过跳板机来连接数据库,一(就)般(我)来(而)说(言)会使用ssh隧道,就轻松能解决这个问题,然鹅,事情并不简单。这里陈述一下:
- 生产环境数据库不让直接访问;
- 跳板机上没有公钥,没有权限;
- 我一次可能需要开3+个隧道才能启动服务【敲重点】
解决
#
本着“我不造轮子,谁来造轮子”的想法,这里就造一个小轮子:用Go
来实现SSH隧道多开,并支持配置。成果预览:
原理简要分析
#
如果代理原理有点了解,这里的原理差不多是一样的:Local <-> SSH tunnel <-> Remote Server,对于隧道来说把Local的请求传给Remote, 把Remote的响应告诉Local。直接上代码:
// Start .
// TODO: support random port by using localhost:0
func (tunnel *SSHTunnel) Start() error {
listener, err := net.Listen("tcp", tunnel.LocalAddr)
if err != nil {
return err
}
defer listener.Close()
// tunnel.Local.Port = listener.Addr().(*net.TCPAddr).Port
for {
conn, err := listener.Accept()
if err != nil {
return err
}
logger.Infof(tunnel.name() + " accepted connection")
go tunnel.forward(conn)
}
}
// 创建隧道并传递消息,分别有两个端点,一个是本地隧道口,另一个是远程服务器上的隧道口
func (tunnel *SSHTunnel) forward(localConn net.Conn) {
// 创建本地到跳板机的SSH连接
serverSSHClient, err := ssh.Dial("tcp", tunnel.ServerAddr, tunnel.SSHConfig)
if err != nil {
logger.Infof(tunnel.name()+" server dial error: %s", err)
return
}
logger.Infof(tunnel.name()+" connected to server=%s (1 of 2)", tunnel.ServerAddr)
// 创建跳板机到远程服务器的连接
remoteConn, err := serverSSHClient.Dial("tcp", tunnel.RemoteAddr)
if err != nil {
logger.Infof(tunnel.name()+" remote dial error: %s", err)
return
}
logger.Infof(tunnel.name()+" connected to remote=%s (2 of 2)", tunnel.RemoteAddr)
copyConn := func(writer, reader net.Conn) {
_, err := io.Copy(writer, reader)
if err != nil {
logger.Infof(tunnel.name()+" io.Copy error: %s", err)
}
}
// local(w) => 远程(r)
go copyConn(localConn, remoteConn)
// 远程(w) => 本地(r)
go copyConn(remoteConn, localConn)
}
代码中需要注意的是:
...
September 21, 2019
一些名词
#
MTU(Maximum Transmission Unit)
#
the maximum transmission unit (MTU) is the size of the largest protocol data unit (PDU) that can be communicated in a single network layer transaction. ——from wiki
MTU 物理接口(数据链路层)提供给其上层(通常是IP层)最大一次传输数据的大小。一般来说MTU=1500byte
。如果MSS + TCP首部 + IP首部 > MTU
,那么IP报文就会存在分片,否则就不需要分片。
MSS (Maximum Segment Size)
#
The maximum segment size (MSS) is a parameter of the options field of the TCP header that specifies the largest amount of data, specified in bytes, that a computer or communications device can receive in a single TCP segment. ——from wiki
...
April 1, 2019
Golang编写的热重载工具,自定义命令,支持监视文件及路径配置,环境变量配置。这是一个重复的轮子~
安装使用
#
go install github.com/yeqown/go-watcher/cmd/go-watcher
命令行
#
➜ go-watcher git:(master) ✗ ./go-watcher -h
NAME:
go-watcher - A new cli application
USAGE:
go-watcher [global options] command [command options] [arguments...]
VERSION:
2.0.0
AUTHOR:
[email protected]
COMMANDS:
init generate a config file to specified postion
run execute a command, and watch the files, if any change to these files, the command will reload
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--help, -h show help
--version, -v print the version
配置文件
#
watcher: # 监视器配置
duration: 2000 # 文件修改时间间隔,只有高于这个间隔才回触发重载
included_filetypes: # 监视的文件扩展类型
- .go #
excluded_regexps: # 不被监视更改的文件正则表达式
- ^.gitignore$
- '*.yml$'
- '*.txt$'
additional_paths: [] # 除了当前文件夹需要额外监视的文件夹
excluded_paths: # 不需要监视的文件名,若为相对路径,只能对于当前路径生效
- vendor
- .git
envs: # 额外的环境变量
- GOROOT=/path/to/your/goroot
- GOPATH=/path/to/your/gopath
使用范例日志
#
➜ go-watcher git:(master) ✗ ./package/osx/go-watcher run -e "make" -c ./config.yml
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/cmd) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/cmd/go-watcher) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/internal) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/internal/command) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/internal/log) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/internal/testdata) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/internal/testdata/exclude) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/internal/testdata/testdata_inner) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/package) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/package/archived) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/package/linux) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/package/osx) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/resources) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/utils) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/utils/testdata) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/utils/testdata/testdata_inner) is under watching
rm -fr package
go build -o package/osx/go-watcher cmd/go-watcher/main.go
GOOS=linux GOARCH=amd64 go build -o package/linux/go-watcher cmd/go-watcher/main.go
mkdir -p package/archived
tar -zcvf package/archived/go-watcher.osx.tar.gz package/osx
a package/osx
a package/osx/go-watcher
tar -zcvf package/archived/go-watcher.linux.tar.gz package/linux
a package/linux
a package/linux/go-watcher
[INFO] command executed done!
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/osx/go-watcher) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/osx) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/linux/go-watcher) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/linux) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/archived/go-watcher.linux.tar.gz) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/archived) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/VERSION) is skipped, not target filetype
[INFO] [/Users/yeqown/Projects/opensource/go-watcher/cmd/go-watcher/main.go] changed
rm -fr package
mkdir -p package/osx
mkdir -p package/linux
echo "2.0.0" > VERSION
cp VERSION package/osx
cp VERSION package/linux
go build -o package/osx/go-watcher cmd/go-watcher/main.go
GOOS=linux GOARCH=amd64 go build -o package/linux/go-watcher cmd/go-watcher/main.go
mkdir -p package/archived
tar -zcvf package/archived/go-watcher.osx.tar.gz package/osx
a package/osx
a package/osx/go-watcher
a package/osx/VERSION
tar -zcvf package/archived/go-watcher.linux.tar.gz package/linux
a package/linux
a package/linux/go-watcher[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/osx) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/linux) is skipped, not target filetype
a package/linux/VERSION
[INFO] command executed done!
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/osx) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/archived) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/VERSION) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package) is skipped, not target filetype
^C[INFO] quit signal captured!
[INFO] go-watcher exited
➜ go-watcher git:(master) ✗
June 28, 2018
“熟悉http协议”,肯定很多IT小伙伴都在招聘岗位上看得到过,但是怎么才叫熟悉http协议呢?抽空梳理了一下,也算是对这一部分知识的笔记吧!
可能对于大部分人来说,网络web编程就是使用一些第三方库来进行请求和响应的处理,再多说一点就是这个URI
要使用POST
方法,对于携带的数据需要处理成为formdata
。
基础知识
#
Q1: HTTP协议是什么?用来干什么?
HTTP协议是基于TCP/IP协议的应用层协议,主要规定了客户端和服务端之间的通信格式。主要作用也就是传输数据(HTML,图片,文件,查询结果)。
#网络分层
#
互联网的实现分成了几层,如何分层有不同的模型(七层,五层,四层),这里按五层模型来解释:
(靠近用户)应用层 < 传输层 < 网络层 < 链接层 < 物理层(靠近硬件)
层级 |
作用 |
拥有协议 |
物理层 |
传送电信号0 1 |
无 |
数据链路层 |
定义数据包;网卡MAC地址;广播的发送方式; |
Ethernet 802.3; Token Ring 802.5 |
网络层 |
引进了IP地址,用于区分不同的计算机是否属于同一网络 |
IP; ARP; RARP |
传输层 |
建立端口到端口的通信,实现程序时间的交流,也就是socket |
TCP; UDP |
应用层 |
约定应用程序的数据格式 |
HTTP; FTP; DNS |
每一层级,都是解决问题而诞生的,也就是他们各自作用对应的问题,推荐参考资料中的“互联网协议入门”。
#HTTP通信流程
#
#拓展–三次握手和四次挥手
#
经常在其他地方看到这些,一直不知道了解这部分有什么用,但是syn Flood
攻击,恰恰是利用了TCP三次握手中的环节。利用假IP伪造SYN
请求,服务端会多次尝试发送SYN-ACK
给客户端,但是IP并不存在也就无法成功建立连接。在一定时间内伪造大量这种请求,会导致服务器资源耗尽无法为正常的连接服务。(注:服务器SYN连接数量有限制,SYN-ACK超时重传机制)
三次握手流程:
- The client requests a connection by sending a SYN (synchronize) message to the server.
- The server acknowledges this request by sending SYN-ACK back to the client.
- The client responds with an ACK, and the connection is established.
...
June 8, 2018
背景和目标
#
背景
#
项目需要在现有项目的基础上实现权限系统,但为了低耦合,选择实现了一个基于ne7ermore/gRBAC的auth-server,用于实现权限,角色,用户的管理,以及提供鉴权服务。在开发环境对接没有问题,正常的鉴权访问。到了线上部署的时候,才发现:
- 线上某服务部署在多台机器上;
- 目前的api-gateway并不支持同一服务配置多个node;
想的办法有:
序号 |
描述 |
优点 |
缺点 |
1 |
api-gateway通过url来转发请求,之前是配置IP加端口 |
api-gateway改动小 |
影响web和APP升级 |
2 |
api-gateway能支持多台机器,并进行调度 |
api-gateway功能更强大,把以后要做的事情提前做好基础 |
好像没啥缺点,只是费点时间支持下多节点配置,并调度 |
如果没说清,请看下图:
目标
#
那么,目标也就明确了,需要实现api-gateway中实现基于权重的调度。为啥要基于权重?其一是仿照nginx基于权重的负载均衡,其二是服务器性能差异。
轮询调度算法介绍
#
轮询调度算法:
#
轮询调度算法的原理是每一次把来自用户的请求轮流分配给内部中的服务器,从1开始,直到N(内部服务器个数),然后重新开始循环。该算法的优点是其简洁性,它无需记录当前所有连接的状态,所以它是一种无状态调度。
假设有一组服务器N台,S = {S1, S2, …, Sn},一个指示变量i表示上一次选择的服务器ID。变量i被初始化为N-1。其算法如下:
j = i;
do {
j = (j + 1) mod n;
i = j;
return Si;
} while (j != i);
return NULL;
平滑加权轮询调度算法:
#
上述的轮询调度算法,并没有考虑服务器性能的差异,实际生产环境中,每一台服务器配置和安装的业务并不一定相同,处理能力不完全一样。因此需要根据服务器能力,分配不同的权值,以免服务的超负荷和过分闲余。
...
May 18, 2018
需要的技术及工具:
- Python3 + Selenuium
- Golang net/http
- React-Native 相关(使用了react-navigation)
- MongoDB
- Redis
代码地址:
项目构思及构成
#
食谱类型的App,应用市场肯定有更好的的食谱APP。所以自己开发的目的,首先是写代码,其次是定制APP~
好的,现在化身产品经理,设计一下APP有哪些功能:
- 每日菜谱推荐,推荐可更换
- 每天需要准备的材料提醒
- 发现更多菜谱
- 分类筛选菜谱
- 搜索菜谱
- 查看菜谱详情
- 设置(不知道设置啥,提前准备吧)
设计稿?不存在的,随心所欲。
现在分析下我需要做的事情:
- 能跑起来的APP,与restful web api 交互。
- 能跑起来的web-api,提供菜谱数据,筛选,推荐,搜索等功能
- 能跑起来的简易spider,从网上获取菜谱信息。(这个爬虫能解析动态生成网站就够用了,姑且称之为爬虫吧)
没有考虑大量数据,因此爬虫并不通用,只适合特定XX网站。
实战爬虫
#
这个APP里面最重要的就是菜谱数据了,那么开发之前,需要明确的数据格式,如下:
{
"name": "name",
"cat": "cat",
"img": "img_url",
"mark_cnt": 19101,
"view_cnt": 181891,
"setps": [
{
"desc": "",
"img": "",
},
// more step
],
"material": {
"ingredients": [
{
"name": "ingredients_name",
"weight": "ingredients_weight",
},
// more ingredients
],
"seasoning": [
{
"name": "seasoning_name",
"weight": "seasoning_weight",
},
// more seasoning
],
},
"create_time": "2018xxxxxx",
"update_time": "2018xxxxxx",
}
目标
#
前提:无法直接获取到该网站的服务API,才使用爬虫间接获取数据。
...
April 20, 2018
关于Gorm
#
gorm文档
遇见问题
#
无法通过结构体的方式更新或查询零值
#
这里零值是说,各个类型的默认值。
关于这一点是在这里中注明了的,也提供了解决方案:
WARNING when update with struct, GORM will only update those fields that with non blank value
For below Update, nothing will be updated as “”, 0, false are blank values of their types
NOTE When query with struct, GORM will only query with those fields has non-zero value, that means if your field’s value is 0, ‘’, false or other zero values, it won’t be used to build query conditions,
...
April 8, 2018
目录
#
Channel
#
一开始是在看channel的源码,结果发现里面含有一些抽象的描述(可能也就是我觉得。。。毕竟没有深入)
Do not change another G’s status while holding this lock
(in particular, do not ready a G), as this can deadlock
with stack shrinking.
其中G
是啥?我看着是很懵逼的,去google了一下,其实是goroutine相关的知识,那就把goroutine理解了先。
2020-04-13 填坑
channel in go
Goroutine
#
G: 表示goroutine,存储了goroutine的执行stack信息、goroutine状态以及goroutine的任务函数等;另外G对象是可以重用的。
P: 表示逻辑processor,P的数量决定了系统内最大可并行的G的数量(前提:系统的物理cpu核数>=P的数量);P的最大作用还是其拥有的各种G对象队列、链表、一些cache和状态。
M: 代表着真正的执行计算资源。在绑定有效的p后,进入schedule循环;而schedule循环的机制大致是从各种队列、p的本地队列中获取G,切换到G的执行栈上并执行G的函数,调用goexit做清理工作并回到m,如此反复。M并不保留G状态,这是G可以跨M调度的基础。M必须关联了P才能执行Go代码。
结合下图更方便理解: –源于Tonybai的博客,见参考资料。
参考资料
#
March 2, 2018
本文主要是总结下在使用aliyun-rds数据备份方案过程中的心得。
高可用一直都是线上服务维护用户体验的关键之一。为了达到高可用,业界已经有了很多方案。最典型的就是“冗余备份+自动故障转移”。冗余备份是说,当一个节点服务不可用时,有其他服务能够代替其工作。除此之外,如果服务出现了必须人工介入解决的故障,也会影响系统的高可用特性。
本文着重介绍数据的高可用方案
数据库冗余
#
如果是单节点的数据库,还用的着说吗?要保证服务高可用,除了主-从数据库之外,还需要从备份数据库,当然不能保证说一定不会遇到所有的备份数据库,都挂掉的情况…。阿里云提供了RDS-高可用版本和RDS-单机版,两者的区别见下图:
这就算最基本的冗余了,没有主从复制,没有读写分离。但是能保证主库在换掉的时候,还能使用备库提供服务。如果服务对于数据库性能和可用性有一定要求,那么可以在这个基础上升个级,见下图:
数据故障自动转移
#
已经有了冗余的数据库节点了,那么接下来要做的事情就是怎么感知数据库异常,并实现自动切换到备份实例中? 阿里云灾备方案的文档是这样描述的:
主实例和灾备实例均搭建主备高可用架构,当主实例所在区域发生突发性自然灾害等状况,主节点(Master)和备节点(Slave)均无法连接时,可将异地灾备实例切换为主实例,在应用端修改数据库链接地址后,即可快速恢复应用的业务访问。
对于主节点全部不可用的情况对应用服务是可见的,因此应用服务可以通过指定一些异常判断,在判定主节点不可用的时候,主动切换数据库连接地址来获取数据,提供服务。
// sql-detect.go
package main
import (
"database/sql"
"fmt"
"sync"
"time"
_ "github.com/go-sql-driver/mysql"
_ "github.com/mxk/go-sqlite"
)
var (
mysqlAvailable bool = true
mutex = sync.Mutex{}
db *sql.DB = nil
)
func MysqlDetection(db *sql.DB, ticker *time.Ticker) {
for {
select {
case <-ticker.C:
if e := db.Ping(); e != nil {
fmt.Println("got error", e)
mutex.Lock()
mysqlAvailable = false
mutex.Unlock()
} else {
fmt.Println("status ok")
}
}
}
}
func MysqlSwitch() {
for {
mutex.Lock()
if !mysqlAvailable {
fmt.Println("Switch Sqlite3")
db, _ = sql.Open("sqlite3", "./foo.db")
}
mutex.Unlock()
time.Sleep(time.Second * 4)
}
}
func main() {
c := make(chan bool)
db, _ = sql.Open("mysql", "yeqiang:yeqiang@/test_yeqiang")
ticker := time.NewTicker(time.Second * 2)
go MysqlDetection(db, ticker)
go MysqlSwitch()
<-c
}
测试截图:
...