开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

fastapi微服务系列(3)-之拦截器grpc_inter

发表于 2021-11-17

上一个小节中讲到了关于拦截器简单的使用,查看官网文档的时候,其实也有讲到关于拦截器的一些说明。这里再补充一下

第三方库封装思路拆解

从官网的文档里,顺腾摸瓜的拆解一下我们的第三方拦截库的一些思路。

1:grpc.ServerInterceptor官网参数说明

官网关于拦截器grpc.ServerInterceptor使用到的RpcMethodHandler自介绍,深入到第三方库的时候其实里面处理返回也是一个RpcMethodHandler

拦截器intercept_service官方的说明:

  • 作用: 在业务执行之前拦截传入的RPC
  • 具体的需要用的额参数有:
+ **continuation** 使用HandlerCallDetails并继续调用链中的下一个拦截器(如果有的话)或RPC处理程序查找逻辑的函数,调用详细信息作为参数传递,如果RPC被认为是服务的,则返回RpcMethodHandler实例,否则不会返回RpcMethodHandler实例。---(来自翻译的结果)
+ **handler\_call\_details** 描述RPC的HandlerCallDetails,对应的是grpc.HandlerCallDetails的类型

上面的意思大概是:需要经过continuation进行包装我们一个handler_call_details才返回一个RpcMethodHandler实例

3:GRPC的HandlerCallDetails

(上小节也有说到)它是一个RPC请求描述,里面包含有两个属性信息:

  • invocation_metadata 客户端传输的元数据
  • method 客户端请求的RPC的方法名

4:GRPC的GenericRpcHandler

  • 它是一个任意多个RPC方法的实现
  • 它需要实现一个抽象方法service,而这个抽象方法需要处理的就是我们的传入的
    一个RPC请求描述HandlerCallDetails,这里面可以对我们的请求进行相关的类型的判断,它是否符合一个我们的RPC支持的各类类型,如果支持的话,则返回一个RpcMethodHandler来表示它是一个可用的RPC的服务请求,如果不是的话,那只能进行打断处理。

5:GRPC的ServiceRpcHandler

  • 它是属于服务的RPC方法的实现
  • 它主要是实现返回服务名称()

一个服务可以有多个方法名,但只有一个服务名称

6:GRPC的RpcMethodHandler

  • 它是一个服务的RPC方法请求实现(意思就是没一个RPC请求理解是为是一个个的RpcMethodHandler 对象
  • 对应我们的GRPC的HandlerCallDetails,它是对RPC请求描述所以RpcMethodHandler 里面包含有对应的HandlerCallDetails
  • 它支持的RPC请求类型主要有以下几个(标识这个请求是属于那种类型的RPC请求)
1
2
diff复制代码- request_streaming
- response_streaming
1
2
diff复制代码- request_deserializer
- response_serializer
1
2
diff复制代码- unary_unary
- unary_stream
1
2
diff复制代码- stream_unary
- stream_stream

一个服务可以有多个方法名,但只有一个服务名称

7:grpc_interceptor第三库的再分析

结合上面对几个对象的了解,结合我们的第三方的库其实可以看得出来,第三方库其实就是对我们的 grpc.RpcMethodHandler进行的封装。而我们的 grpc.RpcMethodHandler是一个RPC方法的实现。

再梳理一下第三方库实现的流程:

image.png

  • 1:定义一个自己的需要实现的抽象类,这个抽象类实现了官网的grpc.ServerInterceptor抽象方法
  • 2:在实现grpc.ServerInterceptor抽象方法intercept_service里面通过
1
ini复制代码next_handler = continuation(handler_call_details)

返回一个RpcMethodHandler对象实例。

image.png

  • 3:_get_factory_and_method 传入的一个RpcMethodHandler对象,我们知道RpcMethodHandler对象包含有具体的类型信息和方法

image.png

image.png

返回的是一个方法函数和一个GRPC的GenericRpcHandler

  • 4:_get_factory_and_method通过对请求类型的判断返回一个我们的是什么请求的额类型和请求方法,如果类型不支持,这直接的抛出RuntimeError错误
  • 5:然后是调用我们的自己需要实现intercept方法传入的请求参数和请求上下文

image.png

  • 6:intercept 我们需要传入参数信息有
  • 自回调的方法
  • 请求对象对象封装
  • 上下文请求提
  • 自定义的方法名称—这个其实好像没用不到

image.png

  • 7:handler_factory 此时对应是我们的一个RpcMethodHandler的方法,重新进行一次的包装再返回一个新的RpcMethodHandler

整个流程大致明白了!有点后知后觉的感觉~哈哈

总结

以上仅仅是个人结合自己的实际需求,做学习的实践笔记!如有笔误!欢迎批评指正!感谢各位大佬!

结尾

END

简书:www.jianshu.com/u/d6960089b…

掘金:juejin.cn/user/296393…

公众号:微信搜【小儿来一壶枸杞酒泡茶】

小钟同学 | 文 【欢迎一起学习交流】| QQ:308711822

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

engines多种负载均衡策略

发表于 2021-11-17

这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战

什么是负载均衡

在我们讲engines的负载均衡之前,我们需要理解一下负载均衡这个概念,它的英文名称是load balance。

这一个概念在单机应用是不存在的,只有在分布式系统或者集群当中才存在,当访问的服务具有多个实例时,需要根据某种“均衡”的策略决定请求发往哪个节点,这就是所谓的负载均衡,原理是将数据流量分摊到多个服务器执行,减轻每台服务器的压力,从而提高了数据的吞吐量。

举一个生活中很常见的例子: 我们去银行取钱,银行只有一个人工服务窗口,几百个人在那边排队取钱。正常情况一个窗口需要12小时才可以把这个每个人取钱需求进行完成。

那如果银行开多一个窗口,那就可以用六个小时就可以完成全部的任务,那再开多一个窗口,那就可以四个小时就可以把全部人的取钱需求进行完成,也可以避免一位某一个银行的服务人员临时有事,导致我们整个银行的取钱不能用。

image.png

常见的负载均衡解决方案

我们知道了什么是负载均衡,我们需要想一下市场上面有哪些负载均衡的解决方案,然后我们应该怎么选择?

像一般的国企或者运营商,他们那边的话基本都会购买对应的硬件来解决,常见的硬件有NetScaler、F5、Radware和Array等商用的负载均衡器,这一些负载均衡器价格比较昂贵,动不动弄就几百万上千万,而且他们的数量还需要很多台。

像这些硬件的负载均衡的话,对于很多的创业公司或者互联网公司可能不太适用,毕竟价格太昂贵。那就通过软件来进行解决,常见的有LVS、Nginx等,它们是基于Linux系统并且开源的负载均衡解决方案。

根据对应的性能还有成本来看Nginx的话,这多数公司的选择。或者使用硬件跟软件互相结合, 比如前面流量入口使用F5服务器,后面的话使用我们的Nginx再做一个负载分发。

Nginx的负载均衡举例

前面说了一个银行取钱的例子,转换到我们后端的请求来看,假如我们的一个服务器的某个接口支持5000的并发,处理耗时可以维持在500毫秒以内。

那如果来了个1万的并发,那我们这个处理耗时就要变成一秒了,随着并发量的增多,我们解决了耗时也会变长;那我们就可以启用多几个服务器,对外提供同样的功能,这样就可以减轻每个服务器的压力,提高接口响应速度和并发。

下面这个图就是Nginx最常用的负载均衡,第一个用户二当家小D请求到Nginx那边,然后Nginx把他分配到ip1那个节点。老王又请求了Nginx,Nginx会根据负载均衡策略把它分发到ip2的那个节点, 以此类推。

这样的话每个后端节点都可以分发到别人的请求,然后也不会造成请求积压,或者单点故障。

图片

Nginx配置负载均衡很简单,下面这一个就是我们的一个例子

图片

1
2
3
4
5
6
7
8
bash复制代码upstream lbs {   
server 192.168.0.106:8080; #节点一
server 192.168.0.106:8081; #节点二
}

location /api/ {
proxy_pass http://lbs;
proxy_redirect default;}

upstream 后面的 lbs 是这个集群的名称,里面可以写很多的应用服务器的ip和跟端口,每个server可以认为是我们后端的一个服务。

底部使用proxy_pass 加 http://lbs 进行转发到对应的集群,用户访问 域名/api/xxx 的时候,会命中Location规则,从而进入对应的集群,按照上面的配置保存好,然后重启我们的Nginx应用即可。

给一个建议方便大家进行一个测试:

1
2
3
复制代码可以用spring boot或者node开发一个应用,对外提供一个接口。
这个接口的主要功能就是用户访问之后 返回当前这个机器的ip跟端口,
这样我们或许做负载均衡测试的时候,就可以知道我们当前访问的是哪个机器。

Nginx常见的负载均衡策略解析

上面我们讲解了Nginx的负载均衡配置,其实负载均衡的策略有很多种,Nginx本身就自带很多种,我们分别来解释一下每一个负载均衡策略的一个作用,还有使用场景。

1、负载均衡策略之节点轮询(默认)

节点轮询这个负载均衡策略是Nginx默认的,它表示每个请求按顺序分配到不同的后端服务器, 比如我们配置了十个服务器。那用户发起10次请求的时候,每个服务器都会收到一次这样的一个请求。

这一种的话会有个缺点 可能造成性能和负载分配不均。像我们后端使用的服务器,有些服务器配置高,有些服务器配置低,配置高的处理请求更快,配置低点处理请求更慢一点,那如果请求量到达上百万上千万的时候,这个差距就慢慢的被拉开。

那这一种策略就比较适合做文件服务器或者你们机器的配置和压力比较均匀的时候。

1
2
3
4
ini复制代码//案例upstream lbs {   
server 192.168.159.133:8080 weight=5;
server 192.168.159.133:8081 weight=10;
}

2、负载均衡策略之weight 权重配置

权重配置负载均衡策略,这个意思就是我们可以使用weight这一个给服务器配置对应的占比。这一个数字越大,分配的流量就越高。

好比我们有些服务器配置比较低,有些服务器配置比较高,那我们就可以给配置高的服务器给他的权重配置高一点,那这样的话他分配的流量就更高,能者多劳嘛!

所以这个负载均衡策略他适合的场景就是服务器性能差异大的情况下进行使用。

1
2
3
4
ini复制代码//案例upstream lbs {   
server 192.168.159.133:8080 weight=5;
server 192.168.159.133:8081 weight=10;
}

3、负载均衡策略之ip_hash(固定分发)

ip_hash, 这一个的意思就是会根据用户的来源的ip进行哈希,然后根据结果再进行分配到对应的服务器上面去, 这样的话每个用户就可以固定的访问到后端的某一个服务器。

那这一种策略的话适合哪一些场景呢?比如我们有些服务器需要做业务分区,或者我们每一个服务器上面的想做本地的缓存,再或者我们的session有单点的情况下就可以使用这一种策略,它的核心就是用户会固定到访问到我们这一个节点,而不会分发到其他的节点。

1
2
3
4
5
ini复制代码upstream lbs {   
ip_hash;
server 192.168.159.133:8080;
server 192.168.159.133:8081;
}

除了上面我们说的三种负载均衡策略之外其实社区里面还有很多种,比如可以根据响应的最短时间进行分发,大家如果对这些感兴趣的话,可以搜索一下Nginx社区里面的负载均衡策略。

额外补充一些知识点,upstream还可以为每个节点设置对应的状态值

  • 比如down 表示当前的server暂时不参与负载
1
2
3
4
5
ini复制代码upstream lbs {   
ip_hash;
server 192.168.159.133:8080 down;
server 192.168.159.133:8081;
}
  • 比如backup ,其它所有的非backup机器down的时候,会请求backup机器,这台机器压力会最轻,配置也会相对低
1
2
3
4
5
ini复制代码upstream lbs {   
ip_hash;
server 192.168.159.133:8080 backup;
server 192.168.159.133:8081;
}

本章小结

  • 掌握什么是负载均衡。
  • 知道Nginx怎么去配置负载均衡。
  • 掌握Nginx的三种常见的负载均衡策略,还有它的使用场景。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

nginx 静态资源部署 【1】 nginx 静态资源部署

发表于 2021-11-17

「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战」。

nginx 静态资源部署 【1】

往期文章:

初识 Nginx

nginx 的安装

nginx 核心配置文件结构

Nginx服务操作的问题

如果想要启动、关闭或重新加载nginx配置文件,都需要先进入到nginx的安装目录的sbin目录,然后使用nginx的二级制可执行文件来操作,相对来说操作比较繁琐,这块该如何优化?另外如果我们想把Nginx设置成随着服务器启动就自动完成启动操作,又该如何来实现?这就需要用到接下来我们要讲解的两个知识点:

1
2
复制代码Nginx配置成系统服务
Nginx命令配置到系统环境

Nginx配置成系统服务

把Nginx应用服务设置成为系统服务,方便对Nginx服务的启动和停止等相关操作,具体实现步骤:

(1) 在/usr/lib/systemd/system目录下添加nginx.service,内容如下:

1
bash复制代码vim /usr/lib/systemd/system/nginx.service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码[Unit]
Description=nginx web service
Documentation=http://nginx.org/en/docs/
After=network.target
​
[Service]
Type=forking
PIDFile=/usr/local/nginx/logs/nginx.pid
ExecStartPre=/usr/local/nginx/sbin/nginx -t -c /usr/local/nginx/conf/nginx.conf
ExecStart=/usr/local/nginx/sbin/nginx
ExecReload=/usr/local/nginx/sbin/nginx -s reload
ExecStop=/usr/local/nginx/sbin/nginx -s stop
PrivateTmp=true
​
[Install]
WantedBy=default.target

(2)添加完成后如果权限有问题需要进行权限设置

1
bash复制代码chmod 755 /usr/lib/systemd/system/nginx.service

(3)使用系统命令来操作Nginx服务

1
2
3
4
5
6
makefile复制代码启动: systemctl start nginx
停止: systemctl stop nginx
重启: systemctl restart nginx
重新加载配置文件: systemctl reload nginx
查看nginx状态: systemctl status nginx
开机启动: systemctl enable nginx

Nginx命令配置到系统环境

前面我们介绍过Nginx安装目录下的二级制可执行文件nginx的很多命令,要想使用这些命令前提是需要进入sbin目录下才能使用,很不方便,如何去优化,我们可以将该二进制可执行文件加入到系统的环境变量,这样的话在任何目录都可以使用nginx对应的相关命令。具体实现步骤如下:

演示可删除

1
2
3
bash复制代码/usr/local/nginx/sbin/nginx -V
cd /usr/local/nginx/sbin nginx -V
如何优化???

(1)修改/etc/profile文件

1
2
3
ruby复制代码vim /etc/profile
在最后一行添加
export PATH=$PATH:/usr/local/nginx/sbin

(2)使之立即生效

1
bash复制代码source /etc/profile

(3)执行nginx命令

1
复制代码nginx -V

Nginx静态资源部署

Nginx静态资源概述

上网去搜索访问资源对于我们来说并不陌生,通过浏览器发送一个HTTP请求实现从客户端发送请求到服务器端获取所需要内容后并把内容回显展示在页面的一个过程。这个时候,我们所请 求的内容就分为两种类型,一类是静态资源、一类是动态资源。 静态资源即指在服务器端真实存在并且能直接拿来展示的一些文件,比如常见的html页面、css文件、js文件、图 片、视频等资源; 动态资源即指在服务器端真实存在但是要想获取需要经过一定的业务逻辑处理,根据不同的条件展示在页面不同这 一部分内容,比如说报表数据展示、根据当前登录用户展示相关具体数据等资源;

Nginx静态资源的配置指令

listen指令

listen:用来配置监听端口。

语法 listen address[:port] [default_server]…; listen port [default_server]…;
默认值 listen *:80
位置 server

listen的设置比较灵活,我们通过几个例子来把常用的设置方式熟悉下:

1
2
3
4
perl复制代码listen 127.0.0.1:8000; // listen localhost:8000 监听指定的IP和端口
listen 127.0.0.1; 监听指定IP的所有端口
listen 8000; 监听指定端口上的连接
listen *:8000; 监听指定端口上的连接

default_server属性是标识符,用来将此虚拟主机设置成默认主机。所谓的默认主机指的是如果没有匹配到对应的address:port,则会默认执行的。如果不指定默认使用的是第一个server。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ini复制代码server{
listen 8080;
server_name 127.0.0.1;
location /{
root html;
index index.html;
}
}
server{
listen 8080 default_server;
server_name localhost;
default_type text/plain;
return 444 'This is a error request';
}

server_name指令

server_name:用来设置虚拟主机服务名称。

127.0.0.1 、 localhost 、域名[<www.baidu.com> | <www.jd.com>]

语法 server_name name …; name可以提供多个中间用空格分隔
默认值 server_name “”;
位置 server

关于server_name的配置方式有三种,分别是:

1
2
3
复制代码精确匹配
通配符匹配
正则表达式匹配

配置方式一:精确匹配

如:

1
2
3
4
5
ini复制代码server {
listen 80;
server_name www.baidu.com www.jd.com;
...
}

补充小知识点:

1
复制代码hosts是一个没有扩展名的系统文件,可以用记事本等工具打开,其作用就是将一些常用的网址域名与其对应的IP地址建立一个关联“数据库”,当用户在浏览器中输入一个需要登录的网址时,系统会首先自动从hosts文件中寻找对应的IP地址,一旦找到,系统会立即打开对应网页,如果没有找到,则系统会再将网址提交DNS域名解析服务器进行IP地址的解析。

windows:C:\Windows\System32\drivers\etc

centos:/etc/hosts

因为域名是要收取一定的费用,所以我们可以使用修改hosts文件来制作一些虚拟域名来使用。需要修改 /etc/hosts文件来添加

1
2
3
bash复制代码vim /etc/hosts
127.0.0.1 www.baidu.com
127.0.0.1 www.jd.com

配置方式二:使用通配符配置

server_name中支持通配符”*“,但需要注意的是通配符不能出现在域名的中间,只能出现在首段或尾段,如:

1
2
3
4
5
6
ini复制代码server {
listen 80;
server_name *.baidu.com www.jd.*;
# www.baidu.com abc.baidu.com www.jd.cn www.jd.com
...
}

下面的配置就会报错

1
2
3
4
5
arduino复制代码server {
listen 80;
server_name www.*.cn www.jd.c*
...
}

配置三:使用正则表达式配置

server_name中可以使用正则表达式,并且使用~作为正则表达式字符串的开始标记。

常见的正则表达式

代码 说明
^ 匹配搜索字符串开始位置
$ 匹配搜索字符串结束位置
. 匹配除换行符\n之外的任何单个字符
\ 转义字符,将下一个字符标记为特殊字符
[xyz] 字符集,与任意一个指定字符匹配
[a-z] 字符范围,匹配指定范围内的任何字符
\w 与以下任意字符匹配 A-Z a-z 0-9 和下划线,等效于[A-Za-z0-9_]
\d 数字字符匹配,等效于[0-9]
{n} 正好匹配n次
{n,} 至少匹配n次
{n,m} 匹配至少n次至多m次
* 零次或多次,等效于{0,}
+ 一次或多次,等效于{1,}
? 零次或一次,等效于{0,1}

配置如下:

1
2
3
4
5
6
7
bash复制代码server{
      listen 80;
      server_name ~^www.(\w+).com$;
      default_type text/plain;
      return 200 $1 $2 ..;
}
注意 ~后面不能加空格,括号可以取值
匹配执行顺序

由于server_name指令支持通配符和正则表达式,因此在包含多个虚拟主机的配置文件中,可能会出现一个名称被多个虚拟主机的server_name匹配成功,当遇到这种情况,当前的请求交给谁来处理呢?

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
arduino复制代码server{
listen 80;
server_name ~^www.\w+.com$;
default_type text/plain;
return 200 'regex_success';
}
​
server{
listen 80;
server_name www.baidu.*;
default_type text/plain;
return 200 'wildcard_after_success';
}
​
server{
listen 80;
server_name *.baidu.com;
default_type text/plain;
return 200 'wildcard_before_success';
}
​
server{
listen 80;
server_name www.baidu.com;
default_type text/plain;
return 200 'exact_success';
}
​
server{
listen 80 default_server;
server_name _;
default_type text/plain;
return 444 'default_server not found server';
}

结论:

1
2
3
4
5
vbscript复制代码exact_success
wildcard_before_success
wildcard_after_success
regex_success
default_server not found server!!
1
2
3
4
5
6
7
8
9
makefile复制代码No1:准确匹配server_name
​
No2:通配符在开始时匹配server_name成功
​
No3:通配符在结束时匹配server_name成功
​
No4:正则表达式匹配server_name成功
​
No5:被默认的default_server处理,如果没有指定默认找第一个server

location指令

1
2
3
4
5
6
7
8
9
10
11
ini复制代码server{
listen 80;
server_name localhost;
location / {

}
location /abc{

}
...
}

location:用来设置请求的URI

| 语法 | location [ = | ~ | * | ^ |@ ] uri{…} |
| — | — |
| 默认值 | — |
| 位置 | server,location |

uri变量是待匹配的请求字符串,可以不包含正则表达式,也可以包含正则表达式,那么nginx服务器在搜索匹配location的时候,是先使用不包含正则表达式进行匹配,找到一个匹配度最高的一个,然后在通过包含正则表达式的进行匹配,如果能匹配到直接访问,匹配不到,就使用刚才匹配度最高的那个location来处理请求。

属性介绍:

不带符号,要求必须以指定模式开始

1
2
3
4
5
6
7
8
9
10
11
12
13
arduino复制代码server {
listen 80;
server_name 127.0.0.1;
location /abc{
default_type text/plain;
return 200 "access success";
}
}
以下访问都是正确的
http://192.168.200.133/abc
http://192.168.200.133/abc?p1=TOM
http://192.168.200.133/abc/
http://192.168.200.133/abcdef

= : 用于不包含正则表达式的uri前,必须与指定的模式精确匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
arduino复制代码server {
listen 80;
server_name 127.0.0.1;
location =/abc{
default_type text/plain;
return 200 "access success";
}
}
可以匹配到
http://192.168.200.133/abc
http://192.168.200.133/abc?p1=TOM
匹配不到
http://192.168.200.133/abc/
http://192.168.200.133/abcdef

~ : 用于表示当前uri中包含了正则表达式,并且区分大小写 ~*: 用于表示当前uri中包含了正则表达式,并且不区分大小写

换句话说,如果uri包含了正则表达式,需要用上述两个符合来标识

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
arduino复制代码server {
listen 80;
server_name 127.0.0.1;
location ~^/abc\w${
default_type text/plain;
return 200 "access success";
}
}
server {
listen 80;
server_name 127.0.0.1;
location ~*^/abc\w${
default_type text/plain;
return 200 "access success";
}
}

^~: 用于不包含正则表达式的uri前,功能和不加符号的一致,唯一不同的是,如果模式匹配,那么就停止搜索其他模式了。

1
2
3
4
5
6
7
8
arduino复制代码 server {
listen 80;
server_name 127.0.0.1;
location ^~/abc{
default_type text/plain;
return 200 "access success";
}
}

设置请求资源的目录root / alias

root:设置请求的根目录

语法 root path;
默认值 root html;
位置 http、server、location

path为Nginx服务器接收到请求以后查找资源的根目录路径。

alias:用来更改location的URI

语法 alias path;
默认值 —
位置 location

path为修改后的根路径。

以上两个指令都可以来指定访问资源的路径,那么这两者之间的区别是什么?

举例说明:

(1)在/usr/local/nginx/html目录下创建一个 images目录,并在目录下放入一张图片mv.png图片

1
2
3
bash复制代码location /images {
root /usr/local/nginx/html;
}

访问图片的路径为:

1
arduino复制代码http://192.168.200.133/images/mv.png

(2)如果把root改为alias

1
2
3
bash复制代码location /images {
alias /usr/local/nginx/html;
}

再次访问上述地址,页面会出现404的错误,查看错误日志会发现是因为地址不对,所以验证了:

1
2
3
4
bash复制代码root的处理结果是: root路径+location路径
/usr/local/nginx/html/images/mv.png
alias的处理结果是:使用alias路径替换location路径
/usr/local/nginx/html/images

需要在alias后面路径改为

1
2
3
bash复制代码location /images {
alias /usr/local/nginx/html/images;
}

(3)如果location路径是以/结尾,则alias也必须是以/结尾,root没有要求

将上述配置修改为

1
2
3
bash复制代码location /images/ {
alias /usr/local/nginx/html/images;
}

访问就会出问题,查看错误日志还是路径不对,所以需要把alias后面加上 /

小结:

1
2
3
4
bash复制代码root的处理结果是: root路径+location路径
alias的处理结果是:使用alias路径替换location路径
alias是一个目录别名的定义,root则是最上层目录的含义。
如果location路径是以/结尾,则alias也必须是以/结尾,root没有要求

index指令

index:设置网站的默认首页

语法 index file …;
默认值 index index.html;
位置 http、server、location

index后面可以跟多个设置,如果访问的时候没有指定具体访问的资源,则会依次进行查找,找到第一个为止。

举例说明:

1
2
3
4
5
perl复制代码location / {
root /usr/local/nginx/html;
index index.html index.htm;
}
访问该location的时候,可以通过 http://ip:port/,地址后面如果不添加任何内容,则默认依次访问index.html和index.htm,找到第一个来进行返回

error_page指令

error_page:设置网站的错误页面

语法 error_page code … [=[response]] uri;
默认值 —
位置 http、server、location……

当出现对应的响应code后,如何来处理。

举例说明:

(1)可以指定具体跳转的地址

1
2
3
arduino复制代码server {
error_page 404 http://www.baidu.com;
}

(2)可以指定重定向地址

1
2
3
4
5
6
7
ini复制代码server{
error_page 404 /50x.html;
error_page 500 502 503 504 /50x.html;
location =/50x.html{
root html;
}
}

(3)使用location的@符合完成错误信息展示

1
2
3
4
5
6
7
kotlin复制代码server{
error_page 404 @jump_to_error;
location @jump_to_error {
default_type text/plain;
return 404 'Not Found Page...';
}
}

可选项=[response]的作用是用来将相应代码更改为另外一个

1
2
3
4
5
6
7
ini复制代码server{
error_page 404 =200 /50x.html;
location =/50x.html{
root html;
}
}
这样的话,当返回404找不到对应的资源的时候,在浏览器上可以看到,最终返回的状态码是200,这块需要注意下,编写error_page后面的内容,404后面需要加空格,200前面不能加空格

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

流量回放工具之GoReplay output-http 源码

发表于 2021-11-17

「这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战」


前言

GoReplay 对数据流的抽象出了两个概念,即用 输入(input ) 和 输出(output ) 来表示数据来源与去向,统称为 plugin,用介于输入和输出模块之间的中间件实现拓展机制。

output_http.go:主要是 HTTP 输出的插件,实现 HTTP 协议, 实现 io.Writer 接口,最后根据配置注册到 Plugin.outputs 队列里。

参数说明

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
bash复制代码-output-http value  //转发进入的请求到一个http地址上
Forwards incoming requests to given http address.
# Redirect all incoming requests to staging.com address
gor --input-raw :80 --output-http http://staging.com
-output-http-elasticsearch string //把请求和响应状态发送到 ElasticSearch
Send request and response stats to ElasticSearch:
gor --input-raw :8080 --output-http staging.com --output-http-elasticsearch 'es_host:api_port/index_name'
-output-http-queue-len int //http输出队列大小
Number of requests that can be queued for output, if all workers are busy. default = 1000 (default 1000)
-output-http-redirects int // 设置多少次重定向被允许,默认忽略
Enable how often redirects should be followed.
-output-http-response-buffer value //最大接收响应大小(缓冲区)
HTTP response buffer size, all data after this size will be discarded.
-output-http-skip-verify
Don't verify hostname on TLS secure connection.
-output-http-stats //每5秒钟输出一次输出队列的状态
Report http output queue stats to console every N milliseconds. See output-http-stats-ms
-output-http-stats-ms int
Report http output queue stats to console every N milliseconds. default: 5000 (default 5000)
-output-http-timeout duration //指定 http 的 request/response 超时时间,默认是 5 秒
Specify HTTP request/response timeout. By default 5s. Example: --output-http-timeout 30s (default 5s)
-output-http-track-response
If turned on, HTTP output responses will be set to all outputs like stdout, file and etc.
-output-http-worker-timeout duration
Duration to rollback idle workers. (default 2s)
-output-http-workers int //gor默认是动态的扩展工作者数量,你也可以指定固定数量的工作者
Gor uses dynamic worker scaling. Enter a number to set a maximum number of workers. default = 0 = unlimited.
-output-http-workers-min int
Gor uses dynamic worker scaling. Enter a number to set a minimum number of workers. default = 1.

默认情况下,Gor 创建一个动态工作池:
它从 10 开始,并在 HTTP 输出队列长度大于 10 时创建更多的 HTTP 输出协程。创建的协程数量(N)等于该工作时间的队列长度检查并发现其长度大于10.每次将消息写入 HTTP 输出队列时都检查队列长度。在产生 N 个协程的请求得到满足之前,不会再有协程创建。如果动态协程池当时不能处理消息,它将睡眠 100 毫秒。如果动态工作协程无法处理消息2秒钟,则会死亡。可以使用 --output-http-workers=20 选项指定固定数量的协程。

HTTP 输出工作数量

NewHTTPOutput 默认情况:
在这里插入图片描述

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
go复制代码// NewHTTPOutput constructor for HTTPOutput
// Initialize workers
func NewHTTPOutput(address string, config *HTTPOutputConfig) PluginReadWriter {
o := new(HTTPOutput)
var err error
config.url, err = url.Parse(address)
if err != nil {
log.Fatal(fmt.Sprintf("[OUTPUT-HTTP] parse HTTP output URL error[%q]", err))
}
if config.url.Scheme == "" {
config.url.Scheme = "http"
}
config.rawURL = config.url.String()
if config.Timeout < time.Millisecond*100 {
config.Timeout = time.Second
}
if config.BufferSize <= 0 {
config.BufferSize = 100 * 1024 // 100kb
}
if config.WorkersMin <= 0 {
config.WorkersMin = 1
}
if config.WorkersMin > 1000 {
config.WorkersMin = 1000
}
if config.WorkersMax <= 0 {
config.WorkersMax = math.MaxInt32 // idealy so large
}
if config.WorkersMax < config.WorkersMin {
config.WorkersMax = config.WorkersMin
}
if config.QueueLen <= 0 {
config.QueueLen = 1000
}
if config.RedirectLimit < 0 {
config.RedirectLimit = 0
}
if config.WorkerTimeout <= 0 {
config.WorkerTimeout = time.Second * 2
}
o.config = config
o.stop = make(chan bool)
//是否收集统计信息,统计输出间隔是多少
if o.config.Stats {
o.queueStats = NewGorStat("output_http", o.config.StatsMs)
}

o.queue = make(chan *Message, o.config.QueueLen)
if o.config.TrackResponses {
o.responses = make(chan *response, o.config.QueueLen)
}
// it should not be buffered to avoid races
o.stopWorker = make(chan struct{})

if o.config.ElasticSearch != "" {
o.elasticSearch = new(ESPlugin)
o.elasticSearch.Init(o.config.ElasticSearch)
}
o.client = NewHTTPClient(o.config)
o.activeWorkers += int32(o.config.WorkersMin)
for i := 0; i < o.config.WorkersMin; i++ {
go o.startWorker()
}
go o.workerMaster()
return o
}

配置后启动 httpclient:
在这里插入图片描述

1
2
3
4
5
go复制代码o.client = NewHTTPClient(o.config)
o.activeWorkers += int32(o.config.WorkersMin)
for i := 0; i < o.config.WorkersMin; i++ {
go o.startWorker()
}

启动多个发送协程:
在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
go复制代码func (o *HTTPOutput) startWorker() {
for {
select {
case <-o.stopWorker:
return
case msg := <-o.queue:
o.sendRequest(o.client, msg)
}
}
}

执行发送:
在这里插入图片描述

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
go复制代码func (o *HTTPOutput) sendRequest(client *HTTPClient, msg *Message) {
if !isRequestPayload(msg.Meta) {
return
}

uuid := payloadID(msg.Meta)
start := time.Now()
resp, err := client.Send(msg.Data)
stop := time.Now()

if err != nil {
Debug(1, fmt.Sprintf("[HTTP-OUTPUT] error when sending: %q", err))
return
}
if resp == nil {
return
}

if o.config.TrackResponses {
o.responses <- &response{resp, uuid, start.UnixNano(), stop.UnixNano() - start.UnixNano()}
}

if o.elasticSearch != nil {
o.elasticSearch.ResponseAnalyze(msg.Data, resp, start, stop)
}
}

发送细节,各种配置生效点:
在这里插入图片描述

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
go复制代码// Send sends an http request using client create by NewHTTPClient
func (c *HTTPClient) Send(data []byte) ([]byte, error) {
var req *http.Request
var resp *http.Response
var err error

req, err = http.ReadRequest(bufio.NewReader(bytes.NewReader(data)))
if err != nil {
return nil, err
}
// we don't send CONNECT or OPTIONS request
if req.Method == http.MethodConnect {
return nil, nil
}

if !c.config.OriginalHost {
req.Host = c.config.url.Host
}

// fix #862
if c.config.url.Path == "" && c.config.url.RawQuery == "" {
req.URL.Scheme = c.config.url.Scheme
req.URL.Host = c.config.url.Host
} else {
req.URL = c.config.url
}

// force connection to not be closed, which can affect the global client
req.Close = false
// it's an error if this is not equal to empty string
req.RequestURI = ""

resp, err = c.Client.Do(req)
if err != nil {
return nil, err
}
if c.config.TrackResponses {
return httputil.DumpResponse(resp, true)
}
_ = resp.Body.Close()
return nil, nil

HTTP 输出队列

在这里插入图片描述

队列用在哪儿呢?
在这里插入图片描述

代码逻辑调用图

在这里插入图片描述

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1379 找出克隆二叉树中的相同节点(java / c++

发表于 2021-11-17

这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战

非常感谢你阅读本文~

欢迎【👍点赞】【⭐收藏】【📝评论】~

放弃不难,但坚持一定很酷~

希望我们大家都能每天进步一点点~

本文由 二当家的白帽子:https://juejin.cn/user/2771185768884824/posts 博客原创~


  1. 找出克隆二叉树中的相同节点:

给你两棵二叉树,原始树 original 和克隆树 cloned,以及一个位于原始树 original 中的目标节点 target。

其中,克隆树 cloned 是原始树 original 的一个 副本 。

请找出在树 cloned 中,与 target 相同 的节点,并返回对该节点的引用(在 C/C++ 等有指针的语言中返回 节点指针,其他语言返回节点本身)。

注意:

  1. 你 不能 对两棵二叉树,以及 target 节点进行更改。
  2. 只能 返回对克隆树 cloned 中已有的节点的引用。

进阶:如果树中允许出现值相同的节点,你将如何解答?

样例 1:

在这里插入图片描述

1
2
3
4
5
6
7
8
makefile复制代码输入: 
tree = [7,4,3,null,null,6,19], target = 3

输出:
3

解释:
上图画出了树 original 和 cloned。target 节点在树 original 中,用绿色标记。答案是树 cloned 中的黄颜色的节点(其他示例类似)。

样例 2:

在这里插入图片描述

1
2
3
4
5
ini复制代码输入: 
tree = [7], target = 7

输出:
7

样例 3:

在这里插入图片描述

1
2
3
4
5
csharp复制代码输入: 
tree = [8,null,6,null,5,null,4,null,3,null,2,null,1], target = 4

输出:
4

样例 4:

在这里插入图片描述

1
2
3
4
5
ini复制代码输入: 
tree = [1,2,3,4,5,6,7,8,9,10], target = 5

输出:
5

样例 5:

在这里插入图片描述

1
2
3
4
5
ini复制代码输入: 
tree = [1,2,null,3], target = 2

输出:
2

提示:

  • 树中节点的数量范围为 [1, 104] 。
  • 同一棵树中,没有值相同的节点。
  • target 节点是树 original 中的一个节点,并且不会是 null 。

分析

  • 这道算法题的需要简单翻译一下,三个参数:第一个参数 original 是原树;第二个参数 cloned 是第一个参数的克隆拷贝;第三个参数 target 是我们要找到的节点,它是第一个参数 original 中的一个节点,需要找到并返回第二个参数 cloned 里对应的节点。
  • 提示中说同一棵树中,没有值相同的节点。所以有的小伙伴可能觉得第一个参数非常多余,二当家也这么觉得。我们直接遍历第二个参数 cloned ,直到找到和第三个参数 target 值相同的节点并返回就可以了。
  • 其实第一个参数在 进阶 挑战里就有用了,如果树中允许出现值相同的节点,那就不能用值去判断是相同节点了。
  • 这时候就需要用到第一个参数 original ,因为第三个参数 target 是原树中的一个节点,所以我们可以直接根据地址判断是否是相同节点。
  • 第二个参数 cloned 是第一个参数的克隆拷贝,所以它们具有相同结构,我们只要按照相同顺序同时遍历原树和克隆树,就可以找到答案。

题解

java

非递归遍历

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
java复制代码/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/

class Solution {
public final TreeNode getTargetCopy(final TreeNode original, final TreeNode cloned, final TreeNode target) {
Deque<TreeNode> stack = new LinkedList<>();
TreeNode node = original;
TreeNode clonedNode = cloned;
while (node != null || !stack.isEmpty()) {
if (node != null) {
if (node == target) {
return clonedNode;
}
stack.push(clonedNode);
stack.push(node);
node = node.left;
clonedNode = clonedNode.left;
} else {
node = stack.pop().right;
clonedNode = stack.pop().right;
}
}
return null;
}
}

递归遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/

class Solution {
public final TreeNode getTargetCopy(final TreeNode original, final TreeNode cloned, final TreeNode target) {
if (cloned == null
|| original == target) {
return cloned;
}
TreeNode ans = getTargetCopy(original.left, cloned.left, target);
if (ans == null) {
ans = getTargetCopy(original.right, cloned.right, target);
}
return ans;
}
}

c++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
cpp复制代码/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/

class Solution {
public:
TreeNode* getTargetCopy(TreeNode* original, TreeNode* cloned, TreeNode* target) {
if (cloned == nullptr
|| original == target) {
return cloned;
}
TreeNode* ans = getTargetCopy(original->left, cloned->left, target);
if (ans == nullptr) {
ans = getTargetCopy(original->right, cloned->right, target);
}
return ans;
}
};

python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None

class Solution:
def getTargetCopy(self, original: TreeNode, cloned: TreeNode, target: TreeNode) -> TreeNode:
if cloned is None or original == target:
return cloned
ans = self.getTargetCopy(original.left, cloned.left, target)
if ans is None:
ans = self.getTargetCopy(original.right, cloned.right, target)
return ans

在这里插入图片描述


原题传送门:https://leetcode-cn.com/problems/find-a-corresponding-node-of-a-binary-tree-in-a-clone-of-that-tree/


本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Go语言搬砖 Storm orm

发表于 2021-11-17

「这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战」。

简介

Storm是嵌入数据库BoltDB的orm

官方宣称BoltDB目前是稳定可靠,可用于生产环境,支持最大1TB数据的嵌入数据库。使用的公司有Shopify和Heroku等

Storm官网: github.com/asdine/stor…

Storm特点

  • 支持创建索引
  • 支持多方式的存储和查找数据
  • 支持高级查询

BoltDB相关资源:

  • BoltDB分支(因主线停止开发): github.com/etcd-io/bbo…
  • 源码阅读小结(中文): github.com/ZhengHe-MD/…
  • 命令行查看数据工具: github.com/br0xen/bolt…
  • web界面查看数据工具: github.com/evnix/boltd…

安装

先使用go将包下载到本地

1
js复制代码go get github.com/asdine/storm/v3

在代码编辑器里面引入既可使用

1
js复制代码import "github.com/asdine/storm/v3"

使用

初始化

Open函数默认创建的db文件权限是0600(属主读写),1秒超时的选项,可以自行传值修改

1
js复制代码//db, err := storm.Open("my.db", storm.BoltOptions(0600, &bolt.Options{Timeout: 1 * time.Second}))

使用默认值初始化,会在项目当前目录创建一个my.db的文件

1
2
3
4
5
6
7
8
9
10
11
js复制代码var (
db *storm.DB
err error
)

func init() {
db, err = storm.Open("my.db")
if err != nil {
panic(err)
}
}

数据模型

  • storm会搜索id或ID做为主键
  • storm:”index” 索引字段
  • storm:”unique” 唯一字段
  • storm:”increment” 自增字段,increment=10从10开始自增
  • storm:”inline” 内联字段,结构体嵌套时使用
  • 没有标识,说明没有唯一属性,也没有索引
1
2
3
4
5
6
7
8
js复制代码type User struct {
ID int
Group string `storm:"index"`
Email string `storm:"unique"`
Name string
Age int `storm:"index"`
CreatedAt time.Time `storm:"index"`
}

写入数据

因为我们没有开启批量写入模式,所以这里只能一条条的写入

为了方便后续的操作,这里写入三条略微不同的数据

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
js复制代码user := User{
ID: 1,
Group: "frontend",
Email: "zhangsan@libaigo.com",
Name: "zhangsan",
Age: 21,
CreatedAt:time.Now(),
}
user2 := User{
ID: 2,
Group: "frontend",
Email: "lisi@libaigo.com",
Name: "lisi",
Age: 32,
CreatedAt:time.Now(),
}
user3 := User{
ID: 3,
Group: "backend",
Email: "zhaowu@libaigo.com",
Name: "zhaowu",
Age: 31,
CreatedAt:time.Now(),
}

_ = db.Save(&user)
_ = db.Save(&user2)
_ = db.Save(&user3)

简单查询

  • One方法 获取一条数据
  • Find方法 获取多条数据
  • All方法 获取全部数据
  • Range方法 获取范围内数据
  • Prefix方法 获取前缀匹配数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
js复制代码var u User
_ = db.One("Name", "zhangsan", &u)
fmt.Println("单条",u)

var u2 []User
_ = db.Find("Group", "frontend", &u2)
fmt.Println("多条",u2)

var u3 []User
_ = db.All(&u3)
fmt.Println("全部",u3)

var u4 []User
_ = db.Range("Age", 30, 40, &u4)
fmt.Println(u4)

var u5 []User
_ = db.Prefix("Name", "z", &u5)
fmt.Println(u5)

跳过,限制,取反

Skip,Limit,Reverse可以在多条,全部,范围查询中进一步过滤出需要的数据范围

1
2
3
4
5
6
7
8
9
js复制代码var u6 []User
_ = db.Find("Group", "frontend", &u6, storm.Skip(10))
_ = db.Find("Group", "frontend", &u6, storm.Limit(10))
_ = db.Find("Group", "frontend", &u6, storm.Reverse())
_ = db.Find("Group", "frontend", &u6, storm.Limit(10), storm.Skip(10), storm.Reverse())

_ = db.All(&u6, storm.Limit(10), storm.Skip(10), storm.Reverse())
_ = db.AllByIndex("CreatedAt", &u6, storm.Limit(10), storm.Skip(10), storm.Reverse())
_ = db.Range("Age", 10, 21, &u6, storm.Limit(10), storm.Skip(10), storm.Reverse())

更新数据

  • Update方法 更新多个字段数据,根据ID来匹配行
  • UpdateField方法 更新单个字段数据,根据ID来匹配行
1
2
js复制代码_ = db.Update(&User{ID:1,Name:"zhangsan",Age:19})
_ = db.UpdateField(&User{ID:3},"Age", 26)

高级查询

复杂的查询Storm写在Select方法中,而该方法中封装匹配器在q包中,所以在使用前需要引入包 import "github.com/asdine/storm/v3/q"

该包中的方法类似于关系运算符和布尔运算符

  • Eq 等于
  • Gt 大于
  • Lte 小于或等于
  • Re 匹配前缀
  • In 精确匹配

使用Select方法接收匹配器,并使用q.And包装,并支持方法链路,最后返回一个查询器

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码q.Eq("Name", "lisi")
q.Gt("Age", 7)
q.Lte("Age", 77)
q.Re("Name", "^z")
q.In("Group", []string{"frontend", "backend"})

//查询例子
query := db.Select(q.Gte("Age", 7), q.Lte("Age", 77)).
Limit(10).Skip(0).OrderBy("Age").Reverse()
var u8 []User
err = query.Find(&u8)
fmt.Println(u8)

删除

  • DeleteStruct方法 删除表里面的数据
  • Drop方法 删除表
  • Init方法 初始化表(用于程序启动固定表结构)
  • ReIndex方法 重建索引(用于表结构变更后)
1
2
3
4
5
6
7
8
js复制代码_  = db.DeleteStruct(&user)

err := db.Drop(&User{})
fmt.Println(err)

err = db.Init(&User{})

err = db.ReIndex(&User{})

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Redis的过期键删除策略

发表于 2021-11-17

这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战

Redis的过期键删除策略

我们知道过期键的删除策略有定时删除、惰性删除和定期删除,redis服务器实际使用的是惰性删除和定期删除

惰性删除

惰性删除策略在db.c的expireIfNeeded

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
c复制代码/*
* 如果 key 已经过期,那么将它删除,否则,不做动作。
*
* key 没有过期时间、服务器正在载入或 key 未过期时,返回 0
* key 已过期,那么返回正数值
*/
int expireIfNeeded(redisDb *db, robj *key) {
// 取出 key 的过期时间
long long when = getExpire(db,key);

// key 没有过期时间,直接返回
if (when < 0) return 0; /* No expire for this key */

/* Don't expire anything while loading. It will be done later. */
// 不要在服务器载入数据时执行过期
if (server.loading) return 0;

/* If we are running in the context of a slave, return ASAP:
* the slave key expiration is controlled by the master that will
* send us synthesized DEL operations for expired keys.
*
* Still we try to return the right information to the caller,
* that is, 0 if we think the key should be still valid, 1 if
* we think the key is expired at this time. */
// 如果服务器作为附属节点运行,那么直接返回
// 因为附属节点的过期是由主节点通过发送 DEL 命令来删除的
// 不必自主删除
if (server.masterhost != NULL) {
// 返回一个理论上正确的值,但不执行实际的删除操作
return mstime() > when;
}

/* Return when this key has not expired */
// 未过期
if (mstime() <= when) return 0;

/* Delete the key */
server.stat_expiredkeys++;

// 传播过期命令
propagateExpire(db,key);

// 从数据库中删除 key
return dbDelete(db,key);
}

所有读写数据库的redis命令在执行之前都会调用expireIfNeeded函数对输入键进行检查,如果输入键已经过期,那么expireIfNeeded函数将输入键从数据库中删除,如果输入键未过期,不删除键

定期删除

定期删除策略在redis,c的activeExpireCycle中,当redis的服务器周期性操作redis.c的serverCron函数的时候调用activeExpireCycle,在规定时间内分多次遍历服务器中的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键

每次运行时,都从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键

全局变量current_db会记录当前activeExpireCycle函数检查的进度,并在下一次activeExpireCycle函数调用时,接着上一次进度进行处理

随着activeExpireCycle的不断执行,服务器的所有数据都会被检查一遍,这时函数将current_db变量重置为0,然后再次进行新一轮的检查

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一种命令行解析的新思路(Go 语言描述) 一 概述 二 现有

发表于 2021-11-17

简介: 本文通过打破大家对命令行的固有印象,对命令行的概念解构后重新梳理,开发出一种功能强大但使用极为简单的命令行解析方法。这种方法支持任意多的子命令,支持可选和必选参数,对可选参数可提供默认值,支持配置文件,环境变量及命令行参数同时使用,配置文件,环境变量,命令行参数生效优先级依次提高,这种设计可以更符合 12 factor的原则。

作者 | 克识

来源 | 阿里技术公众号

一 概述

命令行解析是几乎每个后端程序员都会用到的技术,但相比业务逻辑来说,这些细枝末节显得并不紧要,如果仅仅追求满足简单需求,命令行的处理会比较简单,任何一个后端程序员都可以信手拈来。Go 标准库提供了 flag 库以供大家使用。

然而,当我们稍微想让我们的命令行功能丰富一些,问题开始变得复杂起来,比如,我们要考虑如何处理可选项和必选项,对于可选项,如何设置其默认值,如何处理子命令,以及子命令的子命令,如何处理子命令的参数等等。

目前,Go 语言中使用最广泛功能最强大的命令行解析库是 cobra,但丰富的功能让 cobra 相比标准库的 flag 而言,变得异常复杂,为了减少使用的复杂度,cobra 甚至提供了代码生成的功能,可以自动生成命令行的骨架。然而,自动生成在节省了开发时间的同时,也让代码变得不够直观。

本文通过打破大家对命令行的固有印象,对命令行的概念解构后重新梳理,开发出一种功能强大但使用极为简单的命令行解析方法。这种方法支持任意多的子命令,支持可选和必选参数,对可选参数可提供默认值,支持配置文件,环境变量及命令行参数同时使用,配置文件,环境变量,命令行参数生效优先级依次提高,这种设计可以更符合 12 factor的原则。

二 现有的命令行解析方法

Go 标准库 flag提供了非常简单的命令行解析方法,定义好命令行参数后,只需要调用 flag.Parse方法即可。

1
2
3
4
5
6
7
8
9
10
11
go复制代码// demo.go
var limit int
flag.IntVar(&limit, "limit", 10, "the max number of results")
flag.Parse()
fmt.Println("the limit is", limit)

// 执行结果
$ go run demo.go
the limit is 10
$ go run demo.go -limit 100
the limit is 100

可以看到, flag 库使用非常简单,定要好命令行参数后,只需要调用 flag.Parse就可以实现参数的解析。在定义命令行参数时,可以指定默认值以及对这个参数的使用说明。

如果要处理子命令,flag 就无能为力了,这时候可以选择自己解析子命令,但更多的是直接使用 cobra 这个库。

这里用 cobra 官方给出的例子,演示一下这个库的使用方法

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
go复制代码package main

import (
"fmt"
"strings"

"github.com/spf13/cobra"
)

func main() {
var echoTimes int

var cmdPrint = &cobra.Command{
Use: "print [string to print]",
Short: "Print anything to the screen",
Long: `print is for printing anything back to the screen.
For many years people have printed back to the screen.`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Print: " + strings.Join(args, " "))
},
}

var cmdEcho = &cobra.Command{
Use: "echo [string to echo]",
Short: "Echo anything to the screen",
Long: `echo is for echoing anything back.
Echo works a lot like print, except it has a child command.`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Echo: " + strings.Join(args, " "))
},
}

var cmdTimes = &cobra.Command{
Use: "times [string to echo]",
Short: "Echo anything to the screen more times",
Long: `echo things multiple times back to the user by providing
a count and a string.`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
for i := 0; i < echoTimes; i++ {
fmt.Println("Echo: " + strings.Join(args, " "))
}
},
}

cmdTimes.Flags().IntVarP(&echoTimes, "times", "t", 1, "times to echo the input")

var rootCmd = &cobra.Command{Use: "app"}
rootCmd.AddCommand(cmdPrint, cmdEcho)
cmdEcho.AddCommand(cmdTimes)
rootCmd.Execute()
}

可以看到子命令的加入让代码变得稍微复杂,但逻辑仍然是清晰的,并且子命令和跟命令遵循相同的定义模板,子命令还可以定义自己子命令。

1
2
3
4
shell复制代码$ go run cobra.go echo times hello --times 3
Echo: hello
Echo: hello
Echo: hello

cobra 功能强大,逻辑清晰,因此得到大家广泛的认可,然而,这里却有两个问题让我无法满意,虽然问题不大,但时时萦怀于心,让人郁郁。

1 参数定义跟命令逻辑分离

从上面 –times的定义可以看到,参数的定义跟命令逻辑的定义(即这里的 Run)是分离的,当我们有大量子命令的时候,我们更倾向把命令的定义放到不同的文件甚至目录,这就会出现命令的定义是分散的,而所有命令的参数定义却集中在一起的情况。

当然,这个问题用 cobra 也很好解决,只要把参数定义从 main函数移动到 init函数,并将 init 函数分散到跟子命令的定义一起即可。比如子命令 times 定义在 times.go文件中,同时在文件中定义 init函数,函数中定义了 times 的参数。然而,这样导致当参数比较多时需要定义大量的全局变量,这对于追求代码清晰简洁无副作用的人来说如芒刺背。

为什么不能像 flag库一样,把参数定义放到命令函数的里面呢?这样代码更紧凑,逻辑更直观。

1
2
3
4
5
csharp复制代码// 为什么我不能写成下面这样呢?
func times(){
cobra.IntVarP(&echoTimes, "times", "t", 1, "times to echo the input")
cobra.Parse()
}

相信大家稍加思考就会明白,times函数只有解析完命令行参数才能调用,这就要求命令行参数要事先定义好,如果把参数定义放到 times,这就意味着只有调用 times函数时才会解析相关参数,这就跟让手机根据外壳颜色变换主题一样无理取闹,可是,真的是这样吗?

2 子命令与父命令的顺序定义不够灵活

在开发有子命令甚至多级子命令的工具时,我们经常面临到底是选择 cmd {resource} {action}还是 cmd {action} {resource}的问题,也就是 resource 和 action 谁是子命令谁是参数的问题,比如 Kubernetes 的设计,就是 action 作为子命令:kubectl get pods … kubectl get deploy …,而对于 action 因不同 resource 而差别很大时,则往往选择 resource 作为子命令, 比如阿里云的命令行工具: aliyun ecs … aliyun ram …

在实际开发过程中,一开始我们可能无法确定action 和 resource 哪个作为子命令会更好,在有多级子命令的情况下这个选择可能会更困难。

在不使用任何库的时候,开发者可能会选择在父命令中初始化相关资源,在子命令中执行代码逻辑,这样父命令和子命令相互调换变得非常困难。 这其实是一种错误的逻辑,调用子命令并不意味着一定要调用父命令,对于命令行工具来说,命令执行完进程就会退出,父命令初始化后的资源,并不会在子命令中重复使用。

cobra 的设计可以让大家规避这个错误逻辑,其子命令需要提供一个 Run 函数,在这个函数,应该实现初始化资源,执行业务逻辑,销毁资源的整个生命周期。然而,cobra 仍然需要定义父命令,即必须定义 echo 命令,才能定义 echo times 这个子命令。实际上,在很多场景下,父命令是没有执行逻辑的,特别是以 resource 作为父命令的场景,父命令的唯一作用就是打印这个命令的用法。

cobra 让子命令和父命令的定义非常简单,但父子调换仍然需要修改其间的链接关系,是否有方法让这个过程更简单一点呢?

三 重新认识命令行

关于命令行的术语有很多,比如参数(argument),标识(flag)和选项(option)等,cobra 的设计是基于以下概念的定义

Commands represent actions, Args are things and Flags are modifiers for those actions.

另外,又基于这些定义延伸出更多的概念,比如 persistent flags代表适用于所有子命令的 flag,local flags 代表只用于当前子命令的 flag, required flags代表必选 flag 等等。

这些定义是 cobra 的核心设计来源,要想解决我上面提到的两个问题,我们需要重新审视这些定义。为此,我们从头开始一步步分析何为一个命令行。

1 命令行只是一个可被 shell 解析执行的字符串

1
ruby复制代码$ cmd arg1 arg2 arg3

命令行及其参数,本质上就是一个字符串而已。字符串的含义是由 shell来解释的,对于 shell来说,一个命令行由命令和参数组成,命令和参数以及参数和参数之间是由空白符分割。

还有别的吗? 没了,没有什么父命令、子命令,也没有什么持久参数、本地参数,一个参数是双横线(–) 、单横线(-)还是其他字符开头,都没有关系,这只是字符串而已,这些字符串由 shell 传递给你要执行的程序,并放到 os.Args (Go 语言)这个数组里。

2 参数、标识与选项

从上面的描述可知,参数(argument)是对命令行后面那一串空白符分隔的字符串的称呼,而一个参数,在命令行中又可以赋予不同的含义。

以横线或双横线开头的参数看起来有些特殊,结合代码来看,这种类型的参数有其独特的作用,就是将某个值跟代码中的某个变量关联起来,这种类型的参数,我们叫做标识(flag)。回想一下,os.Args 这个数组里的参数有很多,这些参数跟命令中的变量是没有直接关系的,而 flag 提供的本质上是一个键值对,我们的代码中,通过把键跟某个变量关联起来,从而实现了对这个变量赋值的功能。

1
2
3
bash复制代码flag.IntVar(&limit, "limit", 10, "the max number of results")

// 变量绑定,当在命令行中指定 -limit 100 的时候,这意味着我们是把 100 这个值,赋予变量 limit

标识(flag)赋予了我们通过命令行直接给代码中某个变量赋值的能力。那么一个新的问题是,如果我没有给这个变量赋值呢,程序还能继续运行下去吗?如果不能继续运行,则这个参数(flag 只是一种特殊的参数)就是必选的,否则就是可选的。还有一种可能,命令行定义了多个变量,任意一个变量有值,程序都可以执行下去,也即是说只要这多个标识中随便指定一个,程序就可以执行,那么这些标识或参数从这个角度讲又可以叫做选项(option)。

经过上面的分析,我们发现参数、标识、选项的概念彼此交织,既有区别又有相近的含义。标识是以横线开头的参数,标识名后面的参数(如果有的话),是标识的值。这些参数可能是必选或可选,或多个选项中的一个,因此这些参数又可以称为选项。

3 子命令

经过上面的分析,我们可以很简单的得出结论,子命令只是一种特殊的参数,这种参数外观上跟其他参数没有任何区别(不像标识用横线开头),但是这个参数会引发特殊的动作或函数(任意动作都可以封装为一个函数)。

对比标识和子命令我们会意外的发现其中的关联:标识关联变量而子命令关联函数!他们具有相同的目的,标识后面的参数,是变量的值,那么子命令后面的所有参数,就是这个函数的参数(并非指语言层面的函数参数)。

更有趣的问题是,为什么标识需要以横线开头?如果没有横线,是否能达成关联变量的目的?这显然可以的,因为子命令就没有横线,对变量的关联和对函数的关联并没有什么区别。本质上,这个关联是通过标识或子命令的名字实现的,那横线起到什么作用呢?

是跟变量关联还是函数关联,仍然是由参数的名字决定的,这是在代码中预先定义的,没有横线一样可以区别标识和子命令,一样可以完成变量或参数的关联。

比如:

1
2
3
4
5
6
7
go复制代码// 不带有横线的参数也可以实现关联变量或函数
for _, arg := range os.Args{
switch arg{
case "limit": // 设置 limit 变量
case "scan": // 调用 scan 函数
}
}

由此可见,标识在核心功能实现上,并没有特殊的作用,横线的作用主要是用来增强可读性。然而需要注意的是,虽然本质上我们可以不需要标识,但一旦有了标识,我们就可以利用其特性实现额外的功用,比如 netstat -lnt这里的 -lnt就是 -l -n -t的语法糖。

4 命令行的构成

经过上面的分析,我们可以把命令行的参数赋予不同的概念

  • 标识(flag):以横线或双横线开头的参数,标识又由标识名和标识参数组成–flagname flagarg
  • 非标识参数
  • 子命令(subcommand),子命令也会有子命令,标识和非标识参数

$ command –flag flagarg subcommand subcmdarg –subcmdfag subcmdflagarg

四 启发式命令行解析

我们来重新审视一下第一个需求,即我们期望任何一个子命令的实现,都跟使用标准库的 flag 一样简单。这也就意味着,只有在执行这个函数的时候,才开始解析其命令行参数。如果我们能把子命令和其他参数区分开来,那么就可以先执行子命令对应的函数,后解析这个子命令的参数。

flag 之所以在 main中调用 Parse, 是因为 shell 已经知道字符串的第一个项是命令本身,后面所有项都是参数,同样的,如果我们能识别出子命令来,那么也可以让以下代码变为可能:

1
2
3
4
csharp复制代码func command(){
// 定义 flags
// 调用 Parse 函数
}

问题的关键是如何将子命令跟其他参数区分开来,其中标识名以横线或双横线开头,可以显而易见的区别开来,其他则需要区分子命令、子命令参数以及标识参数。仔细思考可以发现,我们虽然期望参数无需预先定义,但子命令是可以预先定义的,通过把非标识名的参数,跟预先定义的子命令比对,则可以识别出子命令来。

为了演示如何识别出子命令,我们以上面 cobra 的代码为例,假设 cobra.go 代码编译为程序 app,那么其命令行可以执行

1
shell复制代码$ app echo times hello --times 3

按 cobra 的概念, times 是 echo 的子命令,而 echo 又是 app 的子命令。我们则把 echo times整体作为 app 的子命令。

1 简单解析流程

  1. 定义echo子命令关联到函数echo, echo times子命令关联到函数 echoTimes
  2. 解析字符串 echo times hello –times 3
  3. 解析第一个参数,通过 echo匹配到我们预定义的 echo子命令,同时发现这也是 echo times命令的前缀部分,此时,只有知道后一个参数是什么,我们才能确定用户调用的是 echo还是 echo times
  4. 解析第二个参数,通过 times我们匹配到 echo times子命令,并且其不再是任何子命令的前缀。此时确定子命令为 echo times,其他所有参数皆为这个子命令的参数。
  5. 如果解析第二个参数为 hello,那么其只能匹配到 echo这个子命令,那么会调用 echo函数而不是 echoTimes函数。

2 启发式探测流程

上面的解析比较简单,但现实情况下,我们往往期望允许标识可以出现在命令行的任意位置,比如,我们期望新加一个控制打印颜色的选项 –color red,从逻辑上讲,颜色选项更多的是对 echo的描述,而非对 times的描述,因此我们期望可以支持如下的命令行:

1
shell复制代码$ app echo --color red times hello --times 3

此时,我们期望调用的子命令仍然是 echo times,然而中间的参数让情况变得复杂起来,因为这里的参数 red可能是 –color的标识参数(red),可能是子命令的一部分,也可能是子命令的参数。更有甚者,用户还可能把参数错误的写为 –color times

所谓启发式的探测,是指当解析到 red参数时,我们并不知道 red到底是子命令(或者子命令的前缀部分),还是子命令的参数,因此我们可以将其假定为子命令的前缀进行匹配,如果匹配不到,则将其当做子命令参数处理。

  1. 解析到 red时,用 echo red搜索预定义的子命令,若搜索不到,则将 red视为参数
  2. 解析 times时,用 echo times搜索预定义的子命令,此时可搜索到 echo times子命令

可以看到 red不需区分是 –color的标识参数,还是子命令的非标识参数,只要其匹配不到任何子命令,则可以确认,其一定是子命令的参数。

3 子命令任意书写顺序

子命令本质上就是一个字符串,我们上面的启发式解析已经实现将任意子命令字符串识别出来,前提是预先对这个字符串进行定义。也就是将这个字符串关联到某个函数。这样的设计使得父命令、子命令只是逻辑上的概念,而跟具体的代码实现毫无关联,我们需要做的就是调整映射而已。

维护映射关系

1
2
3
4
5
ini复制代码# 关联到 echoTimes 函数
"echo times" => echoTimes

# 调整子命令只是改一下这个映射而已
"times echo" => echoTimes

五 Cortana: 基于启发式命令行解析的实现

为了实现上述思路,我开发了 Cortana这个项目。Cortana 引入 Btree 建立子命令与函数之间的映射关系,得益于其前缀搜索的能力,用户输入任意子命令前缀,程序都会自动列出所有可用的子命令。启发式命令行解析机制,可以在解析具体的标识或子命令参数前,先解析出子命令,从而搜索到子命令所映射的函数,在映射的函数中,去真正的解析子命令的参数,实现变量的绑定。另外,Cortana 充分利用了 Go 语言 Struct Tag 的特性,简化了变量绑定的流程。

我们用 cortana 重新实现 cobra 代码的功能

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
go复制代码package main

import (
"fmt"
"strings"

"github.com/shafreeck/cortana"
)

func print() {
cortana.Title("Print anything to the screen")
cortana.Description(`print is for printing anything back to the screen.
For many years people have printed back to the screen.`)
args := struct {
Texts []string `cortana:"texts"`
}{}

cortana.Parse(&args)
fmt.Println(strings.Join(args.Texts, " "))
}

func echo() {
cortana.Title("Echo anything to the screen")
cortana.Description(`echo is for echoing anything back.
Echo works a lot like print, except it has a child command.`)
args := struct {
Texts []string `cortana:"texts"`
}{}

cortana.Parse(&args)
fmt.Println(strings.Join(args.Texts, " "))
}

func echoTimes() {
cortana.Title("Echo anything to the screen more times")
cortana.Description(`echo things multiple times back to the user by providing
a count and a string.`)
args := struct {
Times int `cortana:"--times, -t, 1, times to echo the input"`
Texts []string `cortana:"texts"`
}{}
cortana.Parse(&args)

for i := 0; i < args.Times; i++ {
fmt.Println(strings.Join(args.Texts, " "))
}
}

func main() {
cortana.AddCommand("print", print, "print anything to the screen")
cortana.AddCommand("echo", echo, "echo anything to the screen")
cortana.AddCommand("echo times", echoTimes, "echo anything to the screen more times")
cortana.Launch()
}

命令用法跟 cobra 完全一样,只是自动生成的帮助信息有一些区别

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
bash复制代码# 不加任何子命令,输出自动生成的帮助信息
$ ./app
Available commands:

print print anything to the screen
echo echo anything to the screen
echo times echo anything to the screen more times

# 默认启用 -h, --help 选项,开发者无需做任何事情
$ ./app print -h
Print anything to the screen

print is for printing anything back to the screen.
For many years people have printed back to the screen.

Usage: print [texts...]

-h, --help help for the command

# echo 任意内容
$ ./app echo hello world
hello world

# echo 任意次数
$ ./app echo times hello world --times 3
hello world
hello world
hello world

# --times 参数可以在任意位置
$ ./app echo --times 3 times hello world
hello world
hello world
hello world

1 选项与默认值

1
2
3
4
go复制代码args := struct {
Times int `cortana:"--times, -t, 1, times to echo the input"`
Texts []string `cortana:"texts"`
}{}

可以看到, echo times 命令有一个 –times 标识,另外,则是要回显的内容,内容本质上也是命令行参数,并且可能因为内容中有空格,而被分割为多个参数。

我们上面提到,标识本质上是将某个值绑定到某个变量,标识的名字,比如这里的 –times,跟变量 args.Times 关联,那么对于非标识的其他参数呢,这些参数是没有名字的,因此我们统一绑定到一个 Slice,也就是 args.Texts

Cortana 定义了属于自己的 Struct Tag,分别用来指定其长标识名、短标识名,默认值和这个选项的描述信息。其格式为: cortana:”long, short, default, description”

  • 长标识名(long): –flagname, 任意标识都支持长标识名的格式,如果不写,则默认用字段名
  • 短标识名(short): -f,可以省略
  • 默认值(default):可以为任意跟字段类型匹配的值,如果省略,则默认为空值,如果为单个横线 “-“,则标识用户必须提供一个值
  • 描述(description):这个选项的描述信息,用于生成帮助信息,描述中可以包含任意可打印字符(包括逗号和空格)

为了便于记忆,cortana这个 Tag 名字也可以写为 lsdd,即上述四部分的英文首字母。

2 子命令与别名

AddCommond 可以添加任意子命令,其本质上是建立子命令与其处理函数的映射关系。

1
bash复制代码cortana.AddCommand("echo", echo, "echo anything to the screen")

在这个例子里,print命令和 echo命令是相同的,我们其实可以通过别名的方式将两者关联

1
2
bash复制代码// 定义 print 为 echo 命令的别名
cortana.Alias("print", "echo")

执行 print 命令实际上执行的是 echo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码$ ./app print -h
Echo anything to the screen

echo is for echoing anything back.
Echo works a lot like print, except it has a child command.

Available commands:

echo times echo anything to the screen more times


Usage: echo [texts...]

-h, --help help for the command

别名的机制非常灵活,可以为任意命令和参数设置别名,比如我们期望实现 three这个子命令,打印任意字符串 3 次。可以直接通过别名的方式实现:

1
2
3
4
5
6
7
shell复制代码cortana.Alias("three", "echo times --times 3")

# three 是 echo times --times 3 的别名
$ ./app three hello world
hello world
hello world
hello world

3 help 标识和命令

Cortana 自动为任意命令生成帮助信息,这个行为也可以通过 cortana.DisableHelpFlag禁用,也可以通过 cortana.HelpFlag来设定自己喜欢的标识名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bash复制代码cortana.Use(cortana.HelpFlag("--usage", "-u"))

# 自定义 --usage 来打印帮助信息
$ ./app echo --usage
Echo anything to the screen

echo is for echoing anything back.
Echo works a lot like print, except it has a child command.

Available commands:

echo times echo anything to the screen more times


Usage: echo [texts...]

-u, --usage help for the command

Cortana 默认并没有提供 help子命令,但利用别名的机制,我们自己很容易实现 help命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
bash复制代码cortana.Alias("help", "--help")

// 通过别名,实现 help 命令,用于打印任意子命令的帮助信息
$ ./app help echo times
Echo anything to the screen more times

echo things multiple times back to the user by providing
a count and a string.

Usage: echo times [options] [texts...]

-t, --times <times> times to echo the input. (default=1)
-h, --help help for the command

4 配置文件与环境变量

除了通过命令行参数实现变量的绑定外,Cortana 还支持用户自定义绑定配置文件和环境变量,Cortana 并不负责配置文件或环境变量的解析,用户可以借助第三方库来实现这个需求。Cortana 在这里的主要作用是根据优先级合并不同来源的值。其遵循的优先级顺序如下:

1
复制代码默认值 < 配置文件 < 环境变量 < 参数

Cortana 设计为便于用户使用任意格式的配置,用户只需要实现 Unmarshaler 接口即可,比如,使用 JSON 作为配置文件:

1
less复制代码cortana.AddConfig("app.json", cortana.UnmarshalFunc(json.Unmarshal))

Cortana 将配置文件或环境变量的解析完全交给第三方库,用户可以自由定义如何将配置文件绑定到变量,比如使用 jsonTag。

5 没有子命令?

Cortana 的设计将命令查找和参数解析解耦,因此两者可以分别独立使用,比如在没有子命令的场景下,直接在main函数中实现参数解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码func main(){
args := struct {
Version bool `cortana:"--version, -v, , print the command version"`
}{}
cortana.Parse(&args)
if args.Version {
fmt.Println("v0.1.1")
return
}
// ...
}

$ ./app --version
v0.1.1

六 总结

命令行解析是一个大家都会用到,但并不是特别重要的功能,除非是专注于命令行使用的工具,一般程序我们都不需要过多关注命令行的解析,所以对于对这篇文章的主题感兴趣,并能读到文章最后的读者,我表示由衷的感谢。

flag库简单易用,cobra 功能丰富,这两个库已经几乎可以满足我们所有的需求。然而,我在编写命令行程序的过程中,总感到现有的库美中不足,flag库只解决标识解析的问题,cobra库虽然支持子命令和参数的解析,但把子命令和参数的解析耦合在一起,导致参数定义跟函数分离。Cortana的核心诉求是将命令查找和参数解析解耦,我通过重新回归命令行参数的本质,发明了启发式解析的方法,最终实现了上述目标。这种解耦使得 Cortana即具备 cobra一样的丰富功能,又有像 flag一样的使用体验。这种通过精巧设计而用非常简单的机制实现强大功能体验让我感到非常舒适,希望通过这篇文章,可以跟大家分享我的快乐。

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

ADG单实例搭建系列之(Active Database Du

发表于 2021-11-17

「这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战」

参考自:Data Guard Physical Standby Setup in Oracle Database 11g Release 2

MOS文档:Step by Step Guide on Creating Physical Standby Using RMAN DUPLICATE…FROM ACTIVE DATABASE (Doc ID 1075908.1)

官方文档:Duplicating Databases

一、Active Database Duplicate步骤(Using Image Copies)

1
2
3
4
5
erlang复制代码1.配置主库DG参数,备库根据主库的PFILE,设置参数值,生成备库SPFILE.
2.配置hosts文件,配置TNS,配置静态监听,添加standby log文件.
3.拷贝主库的密码文件至备库,备库创建PFILE中不存在的目录.
4.把备库启动到nomount状态.
5.RMAN同时连接主库与备库,执行duplicate命令.

Description of Figure 25-4 follows

注:由于Active Database Duplicate无需提前备份,而是通过网络在线copy数据库文件,对主库的CPU等负载要求较高,因此最好在空闲时间进行操作,对于TB级别的数据库,使用Active Duplicate进行DG搭建效率较高,节省空间,但是对网络要求较高;源库必须使用SPFILE。

二、环境准备

主机名 ip DB Version db_name db_unique_name
主库 orcl 192.168.56.120 11.2.0.4 orcl orcl
备库 orcl_stby 192.168.56.121 11.2.0.4 orcl orcl_stby

Notes:

1、db_unique_name主备库不能相同。

2、db_name主备库需保持一致。

3、主备库DB版本需保持一致。

三、搭建过程

1、Oracle软件安装

主库一键安装:

1
bash复制代码./AllOracleSilent.sh -i 192.168.56.120 -d 11g -n orcl -o orcl -b /u01/app -s AL32UTF8

备库一键安装:(备库仅安装ORACLE软件,不建库)

1
bash复制代码./AllOracleSilent.sh -i 192.168.56.121 -d 11g -w Y -n orcl_stby -o orcl -b /u01/app -s AL32UTF8

一键安装脚本可参考:ORACLE一键安装单机11G/12C/18C/19C并建库脚本

2、环境配置

a.配置hosts文件

主库:

1
2
3
4
5
bash复制代码cat <<EOF >> /etc/hosts
##FOR DG BEGIN
192.168.56.121 orcl_stby
##FOR DG END
EOF

备库:

1
2
3
4
5
bash复制代码cat <<EOF >> /etc/hosts
##FOR DG BEGIN
192.168.56.120 orcl
##FOR DG END
EOF

b.配置静态监听和TNS

主库+备库:

Notes:注意这里的GLOBAL_DBNAME和service_name保持一致,即备库需要改为 orcl_stby。

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
bash复制代码##listener.ora
su - oracle -c "cat <<EOF >> /u01/app/oracle/product/11.2.0/db/network/admin/listener.ora
##FOR DG BEGIN
SID_LIST_LISTENER =
(SID_LIST =
(SID_DESC =
(GLOBAL_DBNAME = orcl)
(ORACLE_HOME = /u01/app/oracle/product/11.2.0/db)
(SID_NAME = orcl)
)
)
##FOR DG END
EOF"

##重启监听
su - oracle -c "lsnrctl stop"
su - oracle -c "lsnrctl start"

##tnsnames.ora
su - oracle -c "cat <<EOF >> /u01/app/oracle/product/11.2.0/db/network/admin/tnsnames.ora
##FOR DG BEGIN
ORCL =
(DESCRIPTION =
(ADDRESS_LIST =
(ADDRESS = (PROTOCOL = TCP)(HOST = orcl)(PORT = 1521))
)
(CONNECT_DATA =
(SERVICE_NAME = orcl)
)
)

ORCL_STBY =
(DESCRIPTION =
(ADDRESS_LIST =
(ADDRESS = (PROTOCOL = TCP)(HOST = orcl_stby)(PORT = 1521))
)
(CONNECT_DATA =
(SERVICE_NAME = orcl_stby)
)
)
##FOR DG BEGIN
EOF"

c.主库配置参数

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
sql复制代码select log_mode,force_logging from gv$database;

LOG_MODE FOR
------------ ---
NOARCHIVELOG NO

--开启归档模式
shutdown immediate
startup mount
alter database archivelog;
alter database open;

--开启强制日志模式
alter database force logging;

--查看数据文件路径是否一致,OMF参数建议关闭
select name from v$datafile;
show parameter db_create_file_dest
alter system reset db_create_file_dest;
--NOTES:如果数据文件路径不一致,duplicate将失败。

--设置DG参数
ALTER SYSTEM SET LOG_ARCHIVE_CONFIG='DG_CONFIG=(ORCL,ORCL_STBY)';
ALTER SYSTEM SET LOG_ARCHIVE_DEST_1='LOCATION=/archivelog VALID_FOR=(ALL_LOGFILES,ALL_ROLES) DB_UNIQUE_NAME=ORCL';
ALTER SYSTEM SET LOG_ARCHIVE_DEST_2='SERVICE=orcl_stby ASYNC VALID_FOR=(ONLINE_LOGFILES,PRIMARY_ROLE) DB_UNIQUE_NAME=ORCL_STBY';
ALTER SYSTEM SET LOG_ARCHIVE_DEST_STATE_2=DEFER;
ALTER SYSTEM SET LOG_ARCHIVE_FORMAT='%t_%s_%r.arc' SCOPE=SPFILE;
ALTER SYSTEM SET LOG_ARCHIVE_MAX_PROCESSES=4;
ALTER SYSTEM SET REMOTE_LOGIN_PASSWORDFILE=EXCLUSIVE SCOPE=SPFILE;
ALTER SYSTEM SET FAL_SERVER=ORCL_STBY;
ALTER SYSTEM SET FAL_CLIENT=ORCL;
ALTER SYSTEM SET DB_FILE_NAME_CONVERT='/oradata/orcl','/oradata/orcl' SCOPE=SPFILE;
ALTER SYSTEM SET LOG_FILE_NAME_CONVERT='/oradata/orcl','/oradata/orcl' SCOPE=SPFILE;
ALTER SYSTEM SET STANDBY_FILE_MANAGEMENT=AUTO;

d.生成备库pfile文件并修改,复制参数文件和密码文件至备库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bash复制代码create pfile='/tmp/initorcl_stby.ora' from spfile;

--修改的部分:
*.db_unique_name=orcl_stby
*.fal_client='ORCL_STBY'
*.fal_server='ORCL'
*.log_archive_config='DG_CONFIG=(ORCL_STBY,ORCL)'
*.log_archive_dest_1='LOCATION=/archivelog VALID_FOR=(ALL_LOGFILES,ALL_ROLES) DB_UNIQUE_NAME=ORCL_STBY'
*.log_archive_dest_2='SERVICE=orcl ASYNC VALID_FOR=(ONLINE_LOGFILES,PRIMARY_ROLE) DB_UNIQUE_NAME=ORCL'

--复制参数文件至备库(备库执行)
scp oracle@orcl:/tmp/initorcl_stby.ora /tmp

--复制密码文件至备库(备库执行),要在oracle用户下复制
su - oracle
scp oracle@orcl:/u01/app/oracle/product/11.2.0/db/dbs/orapworcl /u01/app/oracle/product/11.2.0/db/dbs

e.主库添加stanby log文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
sql复制代码set line222
col member for a60
select t2.thread#,t1.group#,t1.member,t2.bytes/1024/1024 from gv$logfile t1,gv$log t2 where t1.group#=t2.group#;

THREAD# GROUP# MEMBER T2.BYTES/1024/1024
---------- ---------- ------------------------------------------------------------ ------------------
1 3 /oradata/orcl/redo03.log 120
1 2 /oradata/orcl/redo02.log 120
1 1 /oradata/orcl/redo01.log 120

--需要注意:
--1.stanby log日志大小与redo log日志保持一致
--2.stanby log数量:
standby logfile=(1+logfile组数)*thread=(1+3)*1=4组,需要加4组standby logfile.
--3.thread要与redo log保持一致,如果是rac,需要增加多个thread对应的standby log

ALTER DATABASE ADD STANDBY LOGFILE thread 1
group 4 ('/oradata/orcl/standby_redo04.log') SIZE 120M,
group 5 ('/oradata/orcl/standby_redo05.log') SIZE 120M,
group 6 ('/oradata/orcl/standby_redo06.log') SIZE 120M,
group 7 ('/oradata/orcl/standby_redo07.log') SIZE 120M;

f.备库创建db目录,开启到nomount状态

1
2
3
4
5
6
7
bash复制代码su - oracle -c "mkdir -p /oradata/orcl"
su - oracle -c "mkdir -p /u01/app/oracle/fast_recovery_area/orcl"
su - oracle -c "mkdir -p /u01/app/oracle/admin/orcl/adump"

sqlplus / as sysdba
create spfile from pfile='/tmp/initorcl_stby.ora';
startup nomount

3、 RMAN DUPLICATE

1
2
3
4
5
6
7
8
9
bash复制代码rman target sys/oracle@orcl AUXILIARY sys/oracle@orcl_stby

run {
allocate channel prmy1 type disk;
allocate channel prmy2 type disk;
allocate auxiliary channel aux1 type disk;
allocate auxiliary channel aux2 type disk;
DUPLICATE TARGET DATABASE FOR STANDBY FROM ACTIVE DATABASE DORECOVER NOFILENAMECHECK;
}

4、备库开启日志应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sql复制代码alter database open read only;

ALTER DATABASE RECOVER MANAGED STANDBY DATABASE USING CURRENT LOGFILE DISCONNECT FROM SESSION;

select database_role,open_mode from v$database;

DATABASE_ROLE OPEN_MODE
---------------- --------------------
PHYSICAL STANDBY READ ONLY WITH APPLY

SQL> SELECT protection_mode FROM v$database;

PROTECTION_MODE
--------------------
MAXIMUM PERFORMANCE

5、主库开启LOG_ARCHIVE_DEST_STATE_2

1
sql复制代码ALTER SYSTEM SET LOG_ARCHIVE_DEST_STATE_2=ENABLE;

6、测试同步情况

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
sql复制代码set line222
col member for a60

--查看是否存在RFS和MRP进程
select process,group#,thread#,sequence# from gv$managed_standby;

--查看standby日志status是否存在active
select t1.group#,t1.thread#,t1.bytes/1024/1024,t1.status,t2.member from gv$standby_log t1,gv$logfile t2 where t1.group#=t2.group#;

--主库建表空间,建用户,建表,增删改测试
create tablespace TEST datafile '/oradata/orcl/test01.dbf' size 100M autoextend off;
create user test identified by test;
grant dba to test;
conn test/test
create table test(id number);
insert into test values (1);
insert into test values (2);
commit;

--备库查看是否同步

SQL> select tablespace_name from dba_tablespaces where tablespace_name='TEST';

TABLESPACE_NAME
------------------------------
TEST

SQL> select username,account_status,created from dba_users where username='TEST';

USERNAME ACCOUNT_STATUS CREATED
------------------------------ -------------------------------- ------------------
TEST OPEN 17-APR-21

SQL> select * from test.test;

ID
----------
1
2

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

REDIS缓存穿透,缓存击穿,缓存雪崩原因+解决方案

发表于 2021-11-17

REDIS缓存穿透,缓存击穿,缓存雪崩原因+解决方案

  • 缓存穿透:key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
  • 缓存击穿:key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
  • 缓存雪崩:当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。

缓存穿透解决方案

一个一定不存在缓存及查询不到的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。

有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

粗暴方式伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ini复制代码//伪代码
public object GetProductListNew() {
int cacheTime = 30;
String cacheKey = "product_list";

String cacheValue = CacheHelper.Get(cacheKey);
if (cacheValue != null) {
return cacheValue;
}

cacheValue = CacheHelper.Get(cacheKey);
if (cacheValue != null) {
return cacheValue;
} else {
//数据库查询不到,为空
cacheValue = GetProductListFromDB();
if (cacheValue == null) {
//如果发现为空,设置个默认值,也缓存起来
cacheValue = string.Empty;
}
CacheHelper.Add(cacheKey, cacheValue, cacheTime);
return cacheValue;
}
}

缓存击穿解决方案

key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题。

使用互斥锁(mutex key)

业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。

SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scss复制代码public String get(key) {
String value = redis.get(key);
if (value == null) { //代表缓存值过期
//设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else {
//这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
get(key); //重试
}
} else {
return value;
}
}

memcache代码:

1
2
3
4
5
6
7
8
9
10
11
scss复制代码if (memcache.get(key) == null) {  
// 3 min timeout to avoid mutex holder crash
if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
value = db.get(key);
memcache.set(key, value);
memcache.delete(key_mutex);
} else {
sleep(50);
retry();
}
}

其它方案:待各位补充。

缓存雪崩解决方案
缓存失效时的雪崩效应对底层系统的冲击非常可怕!大多数系统设计者考虑用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

加锁排队,伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ini复制代码//伪代码
public object GetProductListNew() {
int cacheTime = 30;
String cacheKey = "product_list";
String lockKey = cacheKey;

String cacheValue = CacheHelper.get(cacheKey);
if (cacheValue != null) {
return cacheValue;
} else {
synchronized(lockKey) {
cacheValue = CacheHelper.get(cacheKey);
if (cacheValue != null) {
return cacheValue;
} else {
//这里一般是sql查询数据
cacheValue = GetProductListFromDB();
CacheHelper.Add(cacheKey, cacheValue, cacheTime);
}
}
return cacheValue;
}
}

加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建期间key是锁着的,这是过来1000个请求999个都在阻塞的。同样会导致用户等待超时,这是个治标不治本的方法!

注意:加锁排队的解决方式分布式环境的并发问题,有可能还要解决分布式锁的问题;线程还会被阻塞,用户体验很差!因此,在真正的高并发场景下很少使用!

随机值伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ini复制代码//伪代码
public object GetProductListNew() {
int cacheTime = 30;
String cacheKey = "product_list";
//缓存标记
String cacheSign = cacheKey + "_sign";

String sign = CacheHelper.Get(cacheSign);
//获取缓存值
String cacheValue = CacheHelper.Get(cacheKey);
if (sign != null) {
return cacheValue; //未过期,直接返回
} else {
CacheHelper.Add(cacheSign, "1", cacheTime);
ThreadPool.QueueUserWorkItem((arg) -> {
//这里一般是 sql查询数据
cacheValue = GetProductListFromDB();
//日期设缓存时间的2倍,用于脏读
CacheHelper.Add(cacheKey, cacheValue, cacheTime * 2);
});
return cacheValue;
}
}

解释说明:

  • 缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存;
  • 缓存数据:它的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据缓存设置为60分钟。这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存。
    关于缓存崩溃的解决方法,这里提出了三种方案:使用锁或队列、设置过期标志更新缓存、为key设置不同的缓存失效时间,还有一种被称为“二级缓存”的解决方法。

六、小结
针对业务系统,永远都是具体情况具体分析,没有最好,只有最合适。

于缓存其它问题,缓存满了和数据丢失等问题,大伙可自行学习。最后也提一下三个词LRU、RDB、AOF,通常我们采用LRU策略处理溢出,Redis的RDB和AOF持久化策略来保证一定情况下的数据安全。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…312313314…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%