整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

斐讯k3官改完全突破端口转发限制的方法

口转发默认是10条,通过我的上一篇文章可以突破到16条,但是再多也没有效果,而除了改html文件,lua脚本是只读的,而且也没看到到底哪里限制住了,因为本身是通过iptables实现的,所以取个巧,直接用命令行脚本设置得了按照如下步骤即可:

1.ssh登陆路由器,执行如下命令:

vi /tmp/port-forward.sh

2.把如下内容复制进去然后保存

oip=`ip a show ppp0 | grep inet | awk '{print }'|tr -d "addr:"`

sip=

sport=

oport=

iptables -t nat -A port_forward -d $oip/32 -p tcp -m tcp --dport $oport -j DNAT --to-destination $sip:$sport

iptables -t nat -A port_forward_ctf -p tcp -m tcp --dport $oport -j SKIPCTF

iptables -t nat -A port_forward_post -s 192.168.1.0/24 -d $sip/32 -p tcp -m tcp --dport $sport -j SNAT --to-source $oip:$oport

3.在高级设置启动任务里按照如下增加一行


这样就可以把192.168.1.134的3603端口映射到外部3603端口,这三个参数依次:内部ip,内部端口,外部端口。

这样每次启动都会执行这个命令进行端口映射,每增加一个端口就在启动任务里增加一行就行,只是每增加一个还需要在路由器中手动先执行一下,不然只能重启路由器!

者:潘梦源


前言

Kruise Rollout[1]是 OpenKruise 社区开源的渐进式交付框架。Kruise Rollout 支持配合流量和实例灰度的金丝雀发布、蓝绿发布、A/B Testing 发布,以及发布过程能够基于 Prometheus Metrics 指标自动化分批与暂停,并提供旁路的无感对接、兼容已有的多种工作负载(Deployment、CloneSet、DaemonSet)。

目前 Kruise Rollout 新增了流量调度支持自定义资源的能力,从而更好的支持渐进式发布中的流量调度。本文将对 Kruise Rollout 所提出的方案进行介绍。


什么是渐进式发布?

渐进式发布(Progressive Delivery)是一种软件部署和发布策略,旨在逐步将新版本或功能引入生产环境,以降低风险并确保系统的稳定性。一些常见的渐进式发布形式如下:

  • 金丝雀发布:在发布时会创建一个金丝雀版本的 Deployment 进行验证,当验证通过后,再进行全量的工作负载升级,并删除金丝雀版本的 Deployment。

  • A/B 测试:按照一定的规则将用户流量切分成 A、B 两个不相交通路,并将导入不同版本的 Pod 实例进行处理,以此来更好地观察、对比或者灰度新版本能力。

金丝雀发布、A/B 测试和蓝绿发布都是逐步测试和评估新功能或变更的策略,它们可以根据具体的需求和场景选择适合的部署和测试策略,并结合流量灰度等技术实现逐步发布和测试新版本或功能。


为什么需要对网关资源提供支持?

Kruise Rollout 目前已经对 Gateway API 提供了支持,那么为什么还需要对不同供应商的网关资源提供支持呢?在解释这个问题之前,我们先来简单介绍一下 Gateway API。

当前社区中不同的供应商都有自己的网关资源,并提出了自己的标准,而 Kubernetes 为了提供一个统一的网关资源标准,构建标准化的,独立于供应商的 API,提出了 Gateway API。目前,尽管 Gateway API 还处于开发阶段,但已经有很多项目表示支持或计划支持 Gateway API。包括:

  • Istio 是最流行的服务网格项目之一,Istio 1.9 版本计划引入实验性的 Gateway API 支持。用户可以通过 Gateway 和 HTTPRoute 资源来配置 Istio 的 Envoy 代理。
  • Apache APISIX 是一个动态、实时、高性能的 API 网关,APISIX 目前支持Gateway API 规范的 v1beta1 版本,用于其 Apache APISIX Ingress Controller。
  • Kong 是一个为混合云和多云环境构建的开源 API 网关,Kong 在 Kong Kubernetes Ingress Controller (KIC) 以及 Kong Gateway Operator 中支持 Gateway API。

然而由于目前 Gateway API 并不能覆盖供应商所提出网关资源的所有功能,并且仍然有大量用户使用供应商提供的网关资源,虽然用户可以通过开发 Gateway API 对网关资源进行适配,但这样的工作量较大,所以仅仅为 Gateway API 提供支持是远远不够的,尽管随着 Gateway API 特性的不断丰富,在未来,使用 Gateway API 将成为一种更加推荐的方式。因此,虽然 Kruise Rollout 目前已经提供了对 Gateway API 的支持,如何对现有供应商多种多样的网关资源提供支持仍然是一个重要的问题。

如何兼容社区多样的网关方案?

当前社区中已经存在许多广泛使用的供应商提供的网关资源,比如:Istio、Kong、Apisix 等,然而正如前文所述,这些资源的配置并没有形成统一的标准,因此无法设计出一套通用的代码对资源进行处理,这种情况给开发人员带来了一些不便和挑战。

argo-rollouts 与 flagger 兼容方案

为了能够兼容更多的社区网关资源,一些方案被提出,例如 flagger、argo-rollouts 为每一种网关资源都提供了代码实现。这些方案的实现相对简单,但也存在一些问题:

  • 面对大量的社区网关资源时,需要消耗大量精力进行实现
  • 每次实现都需要重新进行发布,自定义能力较差
  • 在某些环境下用户可能使用定制的网关资源,在这种情况下难以适配
  • 每一种资源都有不同的配置规则,配置较为复杂
  • 每添加一个新的网关资源都需要为其实现新的接口,维护难度较大

argo-rollouts 不同资源配置

因此,需要一种支持用户定制,可以灵活插拔的实现方案,以适配社区以及用户定制的多种多样的网关资源,来满足社区不同的用户的需求,增强 Kruise Rollout 的兼容性和扩展性。

为此,我们提出了一种基于 Lua 脚本的网关资源可扩展流量调度方案


Kruise Rollout:基于 Lua 脚本的可扩展流量调度

Kruise Rollout 使用基于 Lua 脚本的网关资源定制方案,本方案通过调用 Lua 脚本根据发布策略和网关资源原始状态来获取并更新资源的期待工作状态(状态包含 spec、labels 以及 annotations),可以使用户能够轻松地适配和集成不同类型的网关资源,而无需修改现有的代码和配置。

本方案对于网关资源的处理可以表示为上图,整个过程可以描述为:

  1. 用户定义了 Rollout 流量灰度规则、需要修改的资源等信息,开始金丝雀发布
  2. 根据 Rollout 配置获取指定资源
  3. 根据资源调用对应的 Lua 脚本
  4. 将资源当前状态转为字符串存入资源 annotation 中,并与发布策略一同输入 Lua 脚本
  5. 利用 Lua 脚本根据当前状态和发布策略处理得到新状态并更新资源
  6. 发布结束后,从 annotation 中获取资源的原始状态对资源进行恢复

通过使用 Kruise Rollout,用户可以:

  • 定制处理网关资源的 Lua 脚本,可以自由的实现对资源的处理逻辑,为更多资源提供支持
  • 利用一套通用的 Rollout 配置模版对不同资源进行配置,降低配置的复杂性,方便用户配置

同时,Kruise Rollout 采用的方案仅需要添加 5 个新接口即可实现对多种多样网关资源的支持。相比之下,其他方案例如 argo-rollouts 则为不同供应商的网关资源提供了不同的接口,对于 Istio 和 Apisix 来说,argo-rollouts 分别提供了 14 个和 4 个新的接口,而且,该方案随着对更多网关资源的支持,接口数量还会持续增长。相比之下,Kruise Rollout 并不需要为新的网关资源提供新的接口,这使得 Kruise Rollout 成为一种更简洁、更易于维护的选择,而不会增加过多的接口负担。同时,编写 Lua 脚本相对于开发 Gateway API 对网关资源进行适配,可以大大减小开发人员的工作量。

以下展示了一个利用 Lua 脚本对 Istio DestinationRule 进行处理的的示例。

  1. 首先定义 rollout 配置文件:
apiVersion: rollouts.kruise.io/v1alpha1
kind: Rollout
...
spec:
  ...
      trafficRoutings:
      - service: mocka
        createCanaryService: false # 使用原有service,不创建新的canary service
        networkRefs: # 需要控制的网关资源
        - apiVersion: networking.istio.io/v1alpha3
          kind: DestinationRule
          name: ds-demo
      patchPodTemplateMetadata:
        labels
          version: canary # 为新版本pod打上label

2. 对 Istio DestinationRule 进行处理的 Lua 脚本为:

local spec = obj.data.spec -- 获取资源的spec,obj.data为资源的状态信息
local canary = {} -- 初始化一条指向新版本的canary路由规则
canary.labels = {} -- 初始化canary路由规则的labels
canary.name = "canary" -- 定义canary路由规则名称
-- 循环处理rollout配置的新版本pod label
for k, v in pairs(obj.patchPodMetadata.labels) do 
  canary.labels[k] = v -- 向canary规则中加入pod label
end
table.insert(spec.subsets, canary) -- 向资源的spec.subsets中插入canary规则
return obj.data -- 返回资源状态

3. 处理完的 DestinationRule 为:

apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
spec:
  ...
  subsets:
    - labels:           # -+
        version: canary #  |- Lua脚本处理后新插入的规则
      name: canary      # -+
    - labels:
        version: base
      name: version-base


Kruise Rollout 进行 Istio 资源流量调度实践

接下来介绍一个利用我们所提出方案对 Istio 进行支持的具体案例。

1. 首先部署如下图所示的服务。该服务由以下几部分构成:

    • 由 Ingress Gateway 作为外部流量网关
    • 通过 VirtualService 和 DestinationRule 将流量调度至 nginx pod 中
    • 利用 ConfigMap 作为主页 nginx pod 的主页

nginx 服务的 deployment 如下所示:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
        version: base
    spec:
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80
        volumeMounts:
        - name: html-volume
          mountPath: /usr/share/nginx/html
      volumes:
      - name: html-volume
        configMap:
          name: nginx-configmap-base # 挂载ConfigMap作为index

2. 创建 rollout 资源,配置发布规则,该 rollout 分为两批发布:

    • 第一批将 20% 的流量转发至新发布的 pod 中
    • 第二批将带有 header version=canary 的流量转发至新版本 pod 中
apiVersion: rollouts.kruise.io/v1alpha1
kind: Rollout
metadata:
  name: rollouts-demo
  annotations:
    rollouts.kruise.io/rolling-style: canary
spec:
  disabled: false
  objectRef:
    workloadRef:
      apiVersion: apps/v1
      kind: Deployment
      name: nginx-deployment
  strategy:
    canary:
      steps:
      - weight: 20 # 第一批转发20%的流量进入新版本pod
      - replicas: 1 # 第二批将包含version=canary header的流量转发入新版本pod
        matches:
        - headers:
          - type: Exact
            name: version
            value: canary
      trafficRoutings:
      - service: nginx-service # 旧版本pod使用的service
        createCanaryService: false # 不创建新的canary service,新旧pod共用一个service
        networkRefs: # 需要修改的网关资源
        - apiVersion: networking.istio.io/v1alpha3
          kind: VirtualService
          name: nginx-vs
        - apiVersion: networking.istio.io/v1beta1
          kind: DestinationRule
          name: nginx-dr
      patchPodTemplateMetadata: # 为新版本pod打上version=canary的label
        labels:
          version: canary

3. 修改 nginx 服务 deployment 中挂载的 ConfigMap 开始金丝雀发布。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
      ...
      volumes:
      - name: html-volume
        configMap:
          name: nginx-configmap-canary # 挂载新的ConfigMap作为index

4. 开始发布第一批,Kruise Rollout 自动调用定义的 Lua 脚本对 VirtualService 和 DestinationRule 资源进行修改,进行流量调度,将 20% 的流量转发至新版本 pod 中,此时整个服务的流量表示为下图所示:

5. 执行命令 kubectl-kruise rollout approve rollout/rollouts-demo,开始发布第二批,Kruise Rollout 自动调用定义的 Lua 脚本对 VirtualService 和 DestinationRule 资源进行修改,进行流量调度,将包含 version=canary header 的流量转发至新版本 pod 中,此时整个服务的流量表示为下图所示:

6. 执行命令 kubectl-kruise rollout approve rollout/rollouts-demo,发布结束,VirtualService 和 DestinationRule 资源恢复至发布前状态,所有流量路由至新版本 pod。


如何利用 Lua 脚本快速配置网关资源的流量调度

在调用 Lua 脚本获取资源状态新状态时,Kruise Rollout 支持两种 Lua 脚本调用方式,分别为:

  • 自定义的 Lua 脚本:用户自定义的,以 ConfigMap 的形式定义并在 Rollout 中调用
  • 已发布的 Lua 脚本:社区通用的、已经稳定的 Lua 脚本,随 Kruise Rollout 打包发布

Kruise Rollout 默认首先查找本地是否存在已发布的 Lua 脚本,这些脚本通常需要设计测试案例进行单元测试验证其可用性,具有更好的稳定性。测试案例的格式如下所示,Kruise Rollout 利用 Lua 脚本根据 rollout 中定义的发布策略对资源原始状态进行处理,得到发布过程中每一步的资源新状态,并与测试案例中 expected 中定义的期待状态进行对比,以验证 Lua 脚本是否按照预期工作。

rollout:
  # rollout配置
original:
  # 资源的原始状态
expected:
  # 发布过程中资源的期待状态

在资源的 Lua 脚本未发布的情况下,用户还可以快速的通过在 ConfigMap 中配置 Lua 脚本的方式由 Kruise Rollout 调用从而对资源进行处理。

apiVersion: v1

kind: ConfigMap

metadata:

  name: kruise-rollout-configuration

  namespace: kruise-rollout

data:

  # 键以lua.traffic.routing.Kind.CRDGroup的形式命名

  "lua.traffic.routing.DestinationRule.networking.istio.io": |

    --- 定义Lua脚本

    local spec = obj.data.spec

    local canary = {}

    canary.labels = {}

    canary.name = "canary"

    for k, v in pairs(obj.patchPodMetadata.labels) do

        canary.labels[k] = v

    end

    table.insert(spec.subsets, canary)

    return obj.data

详细的 Lua 脚本配置说明参见 Kruise Rollout 官网[2]


未来规划

  • 更多网关协议支持:Kruise Rollout 目前是以 Lua 脚本插件化的方式支持多类型的网关协议,我们后续会重点加大这方面的投入,但面对百花齐放的协议类型,单靠社区 Maintainer 的单薄力量还远远不够,希望更多的社区小伙伴加入我们,一起来不断完善这方面的内容。
  • 全链路灰度支持:全链路灰度是具有更加细粒度和全面的灰度发布模式,它涵盖了应用程序的所有服务,而不止对单一的服务进行灰度,可以更好的对新服务进行模拟和测试。目前可以通过社区的网关资源如 Istio 进行配置来实现,但人工配置往往需要消耗较大的精力。我们将对这一部分进行探索,从而实现对全链路灰度的支持。


社区参与

非常欢迎你通过 Github/Slack/钉钉/微信 等方式加入我们来参与 OpenKruise 开源社区。

你是否已经有一些希望与我们社区交流的内容呢?可以在我们的社区双周会[3]上分享你的声音,或通过以下渠道参与讨论:

  • 加入社区 Slack channel [4](English)
  • 加入社区钉钉群:搜索群号 23330762 (Chinese)
  • 加入社区微信群(新):添加用户 openkruise 并让机器人拉你入群 (Chinese)

相关链接:

[1] Kruise Rollout

https://github.com/openkruise/rollouts

[2] Kruise Rollout 官网

https://openkruise.io/rollouts/introduction

[3] 社区双周会

https://shimo.im/docs/gXqmeQOYBehZ4vqo

[4] Slack channel

https://kubernetes.slack.com/?redir=%2Farchives%2Fopenkruise

、什么是事务?

简单来说,事务(transaction)是指单个逻辑单元执行的一系列操作。

1.1、事务的四大特性ACID

事务有如下四大特性:

  • 1、原子性(Atomicity): 构成事务的所有操作都必须是一个逻辑单元,要么全部执行,要么全不执行
  • 2、一致性(Consistency): 数据库在事务执行前后状态都必须是稳定的或者一致的。A(1000)给B(200)转账100后A(900),B(300)总和保持一致。
  • 3、隔离性(Isolation): 事务之间相互隔离,互不影响。
  • 4、持久性(Durability): 事务执行成功后数据必须写入磁盘,宕机重启后数据不会丢失。

2、Redis中的事务

Redis中的事务通过multi,exec,discard,watch这四个命令来完成。

Redis的单个命令都是原子性的,所以确保事务的就是多个命令集合一起执行。

Redis命令集合打包在一起,使用同一个任务确保命令被依次有序且不被打断的执行,从而保证事务性。

Redis是弱事务,不支持事务的回滚。

2.1、事务命令

事务命令简介

  • 1、multi(开启事务)
    • 用于表示事务块的开始,Redis会将后续的命令逐个放入队列,然后使用exec后,原子化的执行这个队列命令。
    • 类似于mysql事务的begin
  • 2、exec(提交事务)
    • 执行命令队列
    • 类似于mysql事务的commit
  • 3、discard(清空执行命令)
    • 清除命令队列中的数据
    • 类似于mysql事务的rollback,但与rollback不一样 ,这里是直接清空队列所有命令,从而不执行。所以不是的回滚。就是个清除。
  • 4、watch
    • 监听一个redis的key 如果key发生变化,watch就能后监控到。如果一个事务中,一个已经被监听的key被修改了,那么此时会清空队列。
  • 5、unwatch
    • 取消监听一个redis的key

事务操作

# 普通的执行多个命令
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set m_name zhangsan
QUEUED
127.0.0.1:6379> hmset m_set name zhangsan age 20 
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK

# 执行命令前清空队列 将会导致事务执行不成功
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set m_name_1 lisi
QUEUED
127.0.0.1:6379> hmset m_set_1 name lisi age 21
QUEUED
# 提交事务前执行了清空队列命令
127.0.0.1:6379> discard
OK
127.0.0.1:6379> exec
(error) ERR EXEC without MULTI

# 监听一个key,并且在事务提交之前改变在另一个客户端改变它的值,也会导致事务失败
127.0.0.1:6379> set m_name_2 wangwu01
OK
127.0.0.1:6379> watch m_name_2
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set m_name_2 wangwu02
QUEUED
# 另外一个客户端在exec之前执行之后,这里会返回nil,也就是清空了队列,而不是执行成功
127.0.0.1:6379> exec
(nil)

# 另外一个客户端在exec之前执行
127.0.0.1:6379> set m_name_2 niuqi
OK

2.2、事务机制分析

我们前面总是在说,Redis的事务命令是打包放在一个队列里的。那么来看一下Redis客户端的数据结构吧。

client数据结构

typedef struct client {
    // 客户端唯一的ID
    uint64_t id;   
    // 客户端状态 表示是否在事务中
    uint64_t flags;         
    // 事务状态
    multiState mstate;
    // ...还有其他的就不一一列举了
} client;

multiState事务状态数据结构

typedef struct multiState {
    // 事务队列 是一个数组,按照先入先出顺序,先入队的命令在前 后入队的命令在后
    multiCmd *commands;     /* Array of MULTI commands */
    // 已入队命令数
    int count;              /* Total number of MULTI commands */
    // ...略
} multiState;

multiCmd事务命令数据结构

/* Client MULTI/EXEC state */
typedef struct multiCmd {
    // 命令的参数
    robj **argv;
    // 参数长度
    int argv_len;
    // 参数个数
    int argc;
    // redis命令的指针
    struct redisCommand *cmd;
} multiCmd;

Redis的事务执行流程图解

Redis的事务执行流程分析

  • 1、事务开始时,在Client中,有属性flags,用来表示是否在事务中,此时设置flags=REDIS_MULTI
  • 2、Client将命令存放在事务队列中,事务本身的一些命令除外(EXEC,DISCARD,WATCH,MULTI)
  • 3、客户端将命令放入multiCmd *commands,也就是命令队列
  • 4、Redis客户端将向服务端发送exec命令,并将命令队列发送给服务端
  • 5、服务端接受到命令队列后,遍历并一次执行,如果全部执行成功,将执行结果打包一次性返回给客户端。
  • 6、如果执行失败,设置flags=REDIS_DIRTY_EXEC, 结束循环,并返回失败。

2.3、监听机制分析

我们知道,Redis有一个expires的字典用于key的过期事件,同样,监听的key也有一个类似的watched_keys字典,key是要监听的key,值是一个链表,记录了所有监听这个key的客户端。

而监听,就是监听这个key是否被改变,如果被改变了,监听这个key的客户端的flags属性就设置为REDIS_DIRTY_CAS。

Redis客户端向服务器端发送exec命令,服务器判断Redis客户端的flags,如果为REDIS_DIRTY_CAS,则清空事务队列。

redis监听机制图解

redis监听key数据结构

回过头再看一下RedisDb类的watched_keys,确实是一个字典,数据结构如下:

typedef struct redisDb {
    dict *dict;                 /* 存储所有的key-value */
    dict *expires;              /* 存储key的过期时间 */
    dict *blocking_keys;        /* blpop存储阻塞key和客户端对象*/
    dict *ready_keys;           /* 阻塞后push,响应阻塞的那些客户端和key */
    dict *watched_keys;         /* 存储watch监控的key和客户端对象 WATCHED keys for MULTI/EXEC CAS */
    int id;                     /* 数据库的ID为0-15,默认redis有16个数据库 */
    long long avg_ttl;          /* 存储对象的额平均ttl(time in live)时间用于统计 */
    unsigned long expires_cursor; /* Cursor of the active expire cycle. */
    list *defrag_later;         /* List of key names to attempt to defrag one by one, gradually. */
    clusterSlotToKeyMapping *slots_to_keys; /* Array of slots to keys. Only used in cluster mode (db 0). */
} redisDb;

2.4、Redis的弱事务性

为什么说Redis是弱事务性呢? 因为如果redis事务中出现语法错误,会暴力的直接清除整个队列的所有命令。

# 在事务外设置一个值为test
127.0.0.1:6379> set m_err_1 test
OK
127.0.0.1:6379> get m_err_1 
"test"
# 开启事务 修改值 但是队列的其他命令出现语法错误  整个事务会被discard
127.0.0.1:6379> multi 
OK
127.0.0.1:6379> set m_err_1 test1
QUEUED
127.0.0.1:6379> sets m_err_1 test2
(error) ERR unknown command `sets`, with args beginning with: `m_err_1`, `test2`, 
127.0.0.1:6379> set m_err_1 test3
QUEUED
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.

# 重新获取值
127.0.0.1:6379> get m_err_1
"test"

我们发现,如果命令队列中存在语法错误,是直接的清除队列的所有命令,并不是进行事务回滚,但是语法错误是能够保证原子性的

再来看一些,如果出现类型错误呢?比如开启事务后设置一个key,先设置为string, 然后再当成列表操作。

# 开启事务
127.0.0.1:6379> multi 
OK
# 设置为字符串
127.0.0.1:6379> set m_err_1 test_type_1
QUEUED
# 当初列表插入两个值
127.0.0.1:6379> lpush m_err_1 test_type_1 test_type_2
QUEUED
# 执行
127.0.0.1:6379> exec
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of valu
# 重新获取值,我们发现我们的居然被改变了,明明,事务执行失败了啊
127.0.0.1:6379> get m_err_1
"test_type_1"

直到现在,我们确定了redis确实不支持事务回滚。因为我们事务失败了,但是命令却是执行成功了。

弱事务总结

  • 1、大多数的事务失败都是因为语法错误(支持回滚)或者类型错误(不支持回滚),而这两种错误,再开发阶段都是可以遇见的
  • 2、Redis为了性能,就忽略了事务回滚。

那么,redis就没有办法保证原子性了吗,当然有,Redis的lua脚本就是对弱事务的一个补充。

3、Redis中的lua脚本

lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

Lua应用场景:游戏开发、独立应用脚本、Web应用脚本、扩展和数据库插件。

OpenResty:一个可伸缩的基于Nginx的Web平台,是在nginx之上集成了lua模块的第三方服务器。

OpenResty是一个通过Lua扩展Nginx实现的可伸缩的Web平台,内部集成了大量精良的Lua库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发(日活千万级别)、扩展性极高的动态Web应用、Web服务和动态网 关。 功能和nginx类似,就是由于支持lua动态脚本,所以更加灵活,可以实现鉴权、限流、分流、日志记 录、灰度发布等功能。

OpenResty通过Lua脚本扩展nginx功能,可提供负载均衡、请求路由、安全认证、服务鉴权、流量控 制与日志监控等服务。

类似的还有Kong(Api Gateway)、tengine(阿里)

3.1、Lua安装(Linux)

lua脚本下载和安装http://www.lua.org/download.html

lua脚本参考文档:http://www.lua.org/manual/5.4/

# curl直接下载
curl -R -O http://www.lua.org/ftp/lua-5.4.4.tar.gz
# 解压
tar zxf lua-5.4.4.tar.gz
# 进入,目录
cd lua-5.4.4
# 编译安装
make all test

编写lua脚本

编写一个lua脚本test.lua,就定义一个本地变量,打印出来即可。

local name = "zhangsan"

print("name:",name)

执行lua脚本

[root@VM-0-5-centos ~]# lua test.lua 
name:   zhangsan

3.2、Redis中使用Lua

Redis从2.6开始,就内置了lua编译器,可以使用EVAL命令对lua脚本进行求值。

脚本命令是原子性的,Redis服务器再执行脚本命令时,不允许新的命令执行(会阻塞,不在接受命令)。、

EVAL命令

通过执行redis的eval命令,可以运行一段lua脚本。

EVAL script numkeys key [key ...] arg [arg ...]

EVAL命令说明

  • 1、script:是一段Lua脚本程序,它会被运行在Redis服务器上下文中,这段脚本不必(也不应该)定义为一个Lua函数。
  • 2、numkeys:指定键名参数的个数。
  • 3、key [key ...]:从EVAL的第三个参数开始算起,使用了numkeys个键(key),表示在脚本中所用到的哪些Redis键(key),这些键名参数可以在Lua中通过全局变量KEYS数组,用1为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推)
  • 4、arg [arg ...]:可以在Lua中通过全局变量ARGV数组访问,访问的形式和KEYS变量类似(ARGV[1] 、 ARGV[2] ,诸如此类)

简单来说,就是

eval lua脚本片段  参数个数(假设参数个数=2)  参数1 参数2  参数1值  参数2值

EVAL命令执行

# 执行一段lua脚本 就是把传入的参数和对应的值返回回去
127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 name age zhangsan 20
1) "name"
2) "age"
3) "zhangsan"
4) "20"

lua脚本中调用redis

我们直到了如何接受和返回参数了,那么lua脚本中如何调用redis呢?

  • 1、redis.call
    • 返回值就是redis命令执行的返回值
    • 如果出错,则返回错误信息,不继续执行
  • 2、redis.pcall
    • 返回值就是redis命令执行的返回值
    • 如果出错,则记录错误信息,继续执行

其实就是redis.call会把异常抛出来,redis.pcall则时捕获了异常,不会抛出去。

lua脚本调用redis设置值

# 使用redis.call设置值
127.0.0.1:6379> eval "return redis.call('set',KEYS[1],ARGV[1])" 1 eval_01 001
OK
127.0.0.1:6379> get eval_01
"001"

EVALSHA命令

前面的eval命令每次都要发送一次脚本本身的内容,从而每次都会编译脚本。

Redis提供了一个缓存机制,因此不会每次都重新编译脚本,可能在某些场景,脚本传输消耗的带宽可能是不必要的。

为了减少带宽的西消耗,Redis实现了evaklsha命令,它的作用和eval一样,只是它接受的第一个参数不是脚本,而是脚本的SHA1校验和(sum)。

所以如何获取这个SHA1的值,就需要提到Script命令。

  • 1、SCRIPT FLUSH :清除所有脚本缓存。
  • 2、SCRIPT EXISTS :根据给定的脚本校验和,检查指定的脚本是否存在于脚本缓存。
  • 3、SCRIPT LOAD :将一个脚本装入脚本缓存,返回SHA1摘要,但并不立即运行它。
  • 4、SCRIPT KILL :杀死当前正在运行的脚本

执行evalsha命令

# 使用script load将脚本内容加载到缓存中,返回sha的值
127.0.0.1:6379> script load "return redis.call('set',KEYS[1],ARGV[1])"
"c686f316aaf1eb01d5a4de1b0b63cd233010e63d"
# 使用evalsha和返回的sha的值 + 参数个数 参数名称和值执行
127.0.0.1:6379> evalsha c686f316aaf1eb01d5a4de1b0b63cd233010e63d 1 eval_02 002
OK
# 获取结果
127.0.0.1:6379> get eval_02
"002"

我们上面都是将脚本写在代码行里面,可以不可以将脚本内容写在xxx.lua中,直接执行呢? 当然是可以的。

使用redis-cli运行外置lua脚本

编写外置脚本test2.lua, 设置值到redis中。

# 脚本内容 也就是设置一个值
return redis.call('set',KEYS[1],ARGV[1])

# 执行结果,可以使用./redis-cli -h 127.0.0.1 -p 6379 指定redis ip、端口等
root@62ddf68b878d:/data# redis-cli --eval /data/test2.lua eval_03 , test03       
OK

利用Redis整合lua脚本,主要是为了保证性能是事务的原子性,因为redis的事务功能确实有些差劲!

4、Redis的脚本复制

Redis如果开启了主从复制,脚本是如何从主服务器复制到从服务器的呢?

首先,redis的脚本复制有两种模式,脚本传播模式和命令传播模式。

在开启了主从,并且开启了AOF持久化的情况下。

4.1、脚本传播模式

其实就是主服务器执行什么脚本,从服务器就执行什么样的脚本。但是如果有当前事件,随机函数等会导致差异。

主服务器执行命令

# 执行多个redis命令并返回
127.0.0.1:6379> eval "local result1 = redis.call('set',KEYS[1],ARGV[1]); local result2 = redis.call('set',KEYS[2],ARGV[2]); return {result1, result2}" 2 eval_test_01 eval_test_02 0001 0002
1) OK
2) OK
127.0.0.1:6379> get eval_test_01
"0001"
127.0.0.1:6379> get eval_test_02
"0002"

那么主服务器将向从服务器发送完全相同的eval命令:

eval "local result1 = redis.call('set',KEYS[1],ARGV[1]); local result2 = redis.call('set',KEYS[2],ARGV[2]); return {result1, result2}" 2 eval_test_01 eval_test_02 0001 0002

注意:在这一模式下执行的脚本不能有时间、内部状态、随机函数等。执行相同的脚本以及参数必须产生相同的效果。在Redis5,也是处于同一个事务中。

4.2、命令传播模式

处于命令传播模式的主服务器会将执行脚本产生的所有写命令用事务包裹起来,然后将事务复制到AOF文件以及从服务器里面.

因为命令传播模式复制的是写命令而不是脚本本身,所以即使脚本本身包含时间、内部状态、随机函数等,主服务器给所有从服务器复制的写命令仍然是相同的。

为了开启命令传播模式,用户在使用脚本执行任何写操作之前,需要先在脚本里面调用以下函数:

redis.replicate_commands()

redis.replicate_commands() 只对调用该函数的脚本有效:在使用命令传播模式执行完当前脚本之后,服务器将自动切换回默认的脚本传播模式。

执行脚本

eval "redis.replicate_commands();local result1 = redis.call('set',KEYS[1],ARGV[1]); local result2 = redis.call('set',KEYS[2],ARGV[2]); return {result1, result2}" 2 eval_test_03 eval_test_04 0003 0004

appendonly.aof文件内容

*1
$5
MULTI
*3
$3
set
$12
eval_test_03
$4
0003
*3
$3
set
$12
eval_test_04
$4
0004
*1
$4
EXEC

可以看到,在一个事务里面执行了我们脚本执行的命令。

同样的道理,主服务器只需要向从服务器发送这些命令就可以实现主从脚本数据同步了。

5、Redis的管道/事务/脚本

  • 1、管道其实就是一次性执行一批命令,不保证原子性,命令都是独立的,属于无状态操作(也就是普通的批处理)
  • 2、事务和脚本是有原子性的,但是事务是弱原子性,lua脚本是强原子性。
  • 3、lua脚本可以使用lua语言编写比较复杂的逻辑。
  • 4、lua脚本的原子性强于事务,脚本执行期间,另外的客户端或其他的任何脚本或命令都无法执行。所以lua脚本的执行事件应该尽可能的短,不然会导致redis阻塞不能做其他工作。

6、小结

Redis的事务是弱事务,多个命令开启事务一起执行性能比较低,且不能一定保证原子性。所以lua脚本就是对它的补充,它主要就是为了保证redis的原子性。

比如有的业务(接口Api幂等性设计,生成token,(取出toker并判断是否存在,这就不是原子操作))我们需要获取一个key, 并且判断这个key是否存在。就可以使用lua脚本来实现。

还有很多地方,我们都需要redis的多个命令操作需要保证原子性,此时lua脚本可能就是一个不二选择。

7、相关文章

本人还写了Redis的其他相关文章,有兴趣的可以点击查看!

  • <<Redis持久化机制分析>>
  • <<Redis的事件处理机制分析>>
  • <<Redis客户端和服务端如何通信?>>
  • <<redis的淘汰机制分析>>
  • <<Redis的底层数据结构分析>>
  • <<Redis的8种数据类型,什么场景使用?>>
  • <<缓存是什么?缓存有哪些分类?使用它的代价是什么?>>
  • <<缓存的6种常见的使用场景>>