0%

在微服务中,为了防止在高并发场景下被大流量冲击,保护下游服务,防止服务过载,需要有控制并发请求数的手段. 限流器常用的算法有令牌桶/漏桶算法/自适应限流. golang 标准库中自带的限流算法的实现,基于令牌桶算法 rate.

令牌桶可以想象成往一个固定容量大小的桶中放 Token(可以理解成门票),系统会以固定速率往桶中放 Token,桶满了则不放.用户从桶中去 Token,如果有 Token(门票)则可以一直取. 如果没有 Token(门票)则需要等待系统发放. 令牌桶在一段时间的总 Token(门票)数是固定的,但峰值数量可以达到桶的容量.

golang time rate 的使用

初始化

1
2
3
4
5
6
7
8
9
10
// 第一个参数 r 代表每秒放入的令牌数
// 第二个参数 b 代表桶的大小
// 以下表示桶的大小为 1,每秒放入10个令牌,那么放入令牌的间隔是 0.1s
limiter := NewLimiter(10,1)

// 也可以使用另一种方法初始化,跟上面的含义一致

limit := Every(100 * time.Millisecond) // 每 100 ms生成一个token

limiter := NewLimiter(limit, 1) // 也是每秒放入10个令牌

获取 Token 方法

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
// WaitN/Wait
func (lim *Limiter) Wait(ctx context.Context) (err error)
func (lim *Limiter) WaitN(ctx context.Context, n int) (err error)
// WaitN 表示阻塞式获取 n 个令牌,可以传递 Context 设置超时时间
// Wait 即 WaitN(ctx,1)


// AllowN/Allow
func (lim *Limiter) Allow() bool
func (lim *Limiter) AllowN(now time.Time, n int) bool
// AllowN 表示是截止到某一时刻否能够获取到 n个令牌,满足返回 true,并消耗 n 个 Token
// 反之则返回 false,不消耗 Token
// Allow 即 AllowN(ctx,1)

// ReserveN/Reserve
func (lim *Limiter) Reserve() *Reservation
func (lim *Limiter) ReserveN(now time.Time, n int) *Reservation
// ReserveN 表示截止到某一刻获取n个token,返回一个 Reservation,提供方法判断是否需要等待多少时间
// Reserve 相当于 ReserveN(time.Now(), 1)

// ReserveN使用
// Usage example:
// r := lim.ReserveN(time.Now(), 1)
// if !r.OK() {
// // Not allowed to act! Did you remember to set lim.burst to be > 0 ?
// return
// }
// time.Sleep(r.Delay())
// Act()

三种方法的使用场景:

  • 如果不想 drop event,可以使用 Reserve,根据返回的Reservation等待一定时间后执行
  • 如果需要遵守最后期限或取消延迟,使用 Wait
  • 如果可以允许丢弃或者忽略超过速率的事件,使用 Allow

实现原理

令牌桶原理图

看起来是有一个goroutine定时放一个队列中放入令牌,我最开始也是这样认为的,这样的好处是比较简单直观.
在 go 中是采用lazyload的方式实现的,通过记录最近一次消费时间跟当前时间进行对比更新Token数目,同时Token数目也不是使用队列,而是通过计数来实现,节省内存.

三种消费方式底层都是通过 reserveN 来实现的.
为什么能通过记录消费时间和计数来实现呢?主要是通过 Token 数可以和时间相互转化.通过limit可以得到以下信息:

  1. 生成N个Token需要多少时间
  2. 在一段时间内,可以生成多少个Token

根据以上两个信息和最近获取时间,当前获取时间来计算获取 n个 Token 需要等待的时间.

参考资料:

背景

做的 iot 项目,需要远程控制设备,使用 websocket 保活. 问题: sim 卡流量有限制,需要降低 sim 卡流量的消耗.

经过优化,流量从原来一次心跳 1k 到 一个月心跳 未优化前

流量消耗

tcp: keep-alive websocket: ping pong go application: heart
网络抓包

使用 wireshark 抓包后,可以很明显的看出流量的使用情况.总共分为三部分.

  1. 服务端每隔 15s 给客户端发送 tcp keep-alive 包. 总共消耗 44 + 56 = 100
  2. 服务端每隔 54s 给客户端发送 websocket ping 包. 总共消耗 58 + 56 + 62 + 56 = 241
  3. 客户端每隔 60s 给服务端发送 websocket heart 包.总共消耗 79 + 56 = 105 每 60s 消耗流量 (100/15+241/54+105/60)*60
    = 772.7 byte

每个月流量 (100/15+241/54+105/60)60602430/1024/1024 = 31.8 Mb

解决方案

根据流量消耗情况,想到以下方案:

  1. 调大 tcp keep-alive
  2. 关闭 websocket ping pong,使用客户端 heart 保活
  3. 客户端 heart 包间隔调长

heart流量链路

流量链路

client => 负载均衡 => k8s ingress => 应用程序

方案存在的问题: 在各级负载均衡中都需要配置,增加 keepalive 时间,保持长连接

相关配置

linux 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
tcp_keepalive_intvl (integer; default: 75; since Linux 2.4)
The number of seconds between TCP keep-alive probes.

tcp_keepalive_probes (integer; default: 9; since Linux 2.2)
The maximum number of TCP keep-alive probes to send before
giving up and killing the connection if no response is
obtained from the other end.

tcp_keepalive_time (integer; default: 7200; since Linux 2.2)
The number of seconds a connection needs to be idle before
TCP begins sending out keep-alive probes. Keep-alives are
sent only when the SO_KEEPALIVE socket option is enabled.
The default value is 7200 seconds (2 hours). An idle
connection is terminated after approximately an additional
11 minutes (9 probes an interval of 75 seconds apart) when
keep-alive is enabled.

Note that underlying connection tracking mechanisms and
application timeouts may be much shorter.

linux 一般都无需配置,使用默认的即可,应用层的心跳包一般也不可能超过 2 小时.

http://nginx.org/en/docs/http/ngx_http_core_module.html#keepalive_timeout

nginx 配置:

  • keepalive_timeout 超过多少时间没有数据,nginx 主动断开连接.
  • keepalive_requests 一个连接能发送的总请求数
  • proxy_http_version 1.1; 跟后端服务器使用的 http 版本,nginx 默认是 1.0,推荐设置为 1.1.特别是对于使用 upstream 设置 keepalive 时,更要设置为 1.1
    版本 官网keepalive
  • proxy-connect-timeout 跟后端服务器连接时间,文档上说不要超过 75 秒.为什么是 75 呢?难道是因为默认的 keepalive_timeout 为 75?待确认
  • proxy_read_timeout 如果后端服务器在这个时间之内没有发送数据,连接将被关闭.是两次成功读取的时间间隔,不是整个连接的时间.
  • proxy_write_timeout 如果nginx在这个时间之内没有发送数据给后端服务器,连接将被关闭.是两次成功写入的时间间隔,不是这个连接的时间.

需要注意的是: tcp keep-alive 探测报文并不能重置 nginx 的 keepalive_timeout 超时时间,一个是 4 层的,一个是 7 层的。那为什么还有发送 keep-alive呢?防止下层设备将连接关闭掉.

k8s ingress 配置跟 nginx 配置基本相同.

设置nginx 与后端服务的长连接.
配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14

upstream cab{
least_conn;
server 192.168.10.119:8080 max_fails=3 fail_timeout=15s;
keepalive_timeout 8s;
keepalive_requests 1000;
keepalive 10;
}
location / {
proxy_pass http://cab;
proxy_http_version 1.1;
proxy_set_header Connection "";
}

  • upstream-keepalive-timeout 配置跟后端服务的长连接超时时间
    在 nginx-configuration 配置后,无需重启 ingress-controller.

添加 nginx ingress annotation

keep-alive

1
2
3
4
5
6
annotations:
nginx.ingress.kubernetes.io/proxy-connect-timeout: "60"
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
nginx.ingress.kubernetes.io/keep-alive: "899"
nginx.ingress.kubernetes.io/keep-alive-requests: "10000"

websocket 应用关闭服务端主动 ping,并增加在 read 数据时,设置 SetWriteDeadlineSetReadDeadline时间,
防止收到客服端 heart 时,服务端未延迟 deadline.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 去除定时器
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
c.conn.Close()
}()
for {
select {
case <-ticker.C:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
log.Error(err)
return
}
}
}

其他问题

nginx 每 15 秒发送 keepalive 包 可能的原因是 docker 发送的,需要查看容器的配置 验证方法: 使用 linux 直接启动 nginx.已验证,在虚拟机中直接启动 nginx,不通过 docker 启动.同样的配置,虚拟机中不会发送 keepliave 包.

aliyun slb 最大超时连接时间 900s,不影响长连接时间.

Nginx 502 Bad Gateway问题分析与踩过的坑

aws 长连接不能超过 350s

在测试过程中,一直想调长 websocket 长连接的时间,但是就是超不过 350s.超过 350 秒客户端会受到服务端的 RST 包,导致连接断开.
查找 aws 文档,跟 NELB 的connection-idle-timeout
设置有关.

1
Elastic Load Balancing sets the idle timeout value for TCP flows to 350 seconds. You cannot modify this value. Clients or targets can use TCP keepalive packets to reset the idle timeout.

NLB 设置死了 350s,不能调整.
aws 监控
aws 负载均衡 Reset 监控
解决措施: 将 NLB 改成4层 ELB(Classic Load Balance),调大 connection idle timeout
相关资料:

最近工作中碰到 centos7 中网络不通的问题,整理下排查网络问题的思路.

网络不通的情况:

  • 第一种: 不能访问公网
  • 第二种: 流量不能进来

第一种不能访问公网的可能的原因:

  • 物理原因
    • 网线问题
      • 没插好
      • 网线质量问题
    • 交换机质量问题
  • 软件配置原因
    • 防火墙配置
    • network 配置
    • route 路由配置
    • DNS 配置问题
      第二种 流量进不来:
  • 物理原因同上
  • 软件配置原因
    • 防火墙配置
    • network 配置
    • route 路由配置

上面两种情况可能的原因很类似,但是排查的方法有不同.

不能访问公网的排查步骤:
1.首先确定是否能访问

1
2
ping qq.com
ping 8.8.8.8

ping 8.8.8.8 有时候可能域名 ping 不通,但是 ip 地址能 ping 通,需检查 DNS 配置是否有问题
2. 确定 dns 是否正常

1
2
3
# 查看自己的 dns 配置
# 使用其他 dns 服务器解析
dig qq.com @114.114.114.114
  1. 查看内网 ip 是否正确,路由器是否分配了 ip 或者 network 配置是否正确
    1
    2
    3
    4
    5
    6
    7
    ip a

    cat /etc/sysconfig/network-scripts/ifcfg-eth0.conf
    ```
    4. 查看 route 配置,使用的网卡是否正确
    ```bash
    route -n

背景

使用 lvm 格式在磁盘 A 上安装 centos7 系统,使用默认的 group 名称.

在 磁盘 A(全部剩余空间) 和磁盘 B 上搭建 lvm mirror 模式.

当磁盘 A 损坏时,希望能迁移出磁盘 B 的数据.最开始想的时将磁盘 B 挂载到安装了新系统的磁盘 C,实际出现 磁盘 C 系统启动失败的问题.

原因

磁盘 C 和磁盘 B 的 group 名称相同,导致系统找不到磁盘 C 的启动卷.

解决方案

方法一: 进入急救系统,修改B 盘 vg 的名称

方法二: 进入系统 C,修改 C 盘 vg的名称,注意需要修改grub和 fstab 的配置
方案三: 重装系统 C,安装的时候使用不重名的 group 名称.

方法一存在的问题:

推荐使用方案二,三,比较方便修改.

洪强宁:从程序员到架构师,从架构师到CTO

优秀架构师的能力:

  1. 取舍 不同业务场景的需求和关注点不同,导致架构设计也是不同的。比如:在线直播关注时延,安全,离线业务关注吞吐率,基于不同场景做取舍。

  2. 前瞻 对未来不确定的事情提前做考虑.既有技术上的发展,也要有基于业务发展的考虑.

  3. 抽象 软件开发是复杂的,需要识别复杂度的来源,分层抽象隔离复杂度.系统全局视角,组件角色定义,不过早进入组件细节中.(写业务代码也是一样,先写伪代码,在实现细节.)

  4. 容错
    后端开发是面向错误的开发.架构师所处的环境就更恶劣了.第一,面向错误设计,事前先做解决方案(解决方案需要事前做好演练,不能等出现问题才发现方案有问题).第二个问题是数据做好备份,没有备份的日子总是提心吊胆.第三,出现故障时如何处理?只换不修,快速切换到正常状态.