mac 上学习k8s系列(10)nginx-ingress lua连redis 失败排出过程

2022-08-02 19:25:27 浏览数 (1)

在学习mac 上学习k8s系列(9)nginx-ingress lua的时候遇到了一个问题nginx-ingress lua连接redis失败,这里涉及到了多个复杂系统间的通信:k8s,nginx ,lua,redis ,golang的后台服务 ,技术栈也跨跃性也很大,从k8s的yaml配置到nginx的conf配置到lua脚本,排查起来非常麻烦,下面介绍下整个问题解决的思路和流程,希望对大家有所启发。

在写完所有配置后测试:

代码语言:javascript复制
% curl -H "token:12344" 127.0.0.1/apple
<h1>系统开小差了</h1>

发现redis连不上,首先通过NodePort的方式,暴露redis服务

代码语言:javascript复制
 % kubectl get svc -o wide |grep redis 
redis  NodePort       10.108.154.209   <none>        6379:30379/TCP               8s     app=redis

通过本地的redis客户端尝试连接下

代码语言:javascript复制
%  redis-cli -h 127.0.0.1 -p 30379 -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:30379>

连接成功,说明我们的redis服务是好的。范围缩小到Ingress的配置和访问方式的问题上了。

查下ingress的日志

代码语言:javascript复制
kubectl logs ingress-nginx-controller-57648496fc-qn2lz  -n <namespace> 

2021/08/28 07:08:44 [emerg] 75#75: io_setup() failed (38: Function not implemented)
192.168.65.3 - - [28/Aug/2021:07:08:56  0000] "GET /apple HTTP/1.1" 500 39 "-" "curl/7.64.1" 91 0.009 [default-apple-service-5678] [] - - - - 1ebb5fd894d7bb64ff7c835c4012358a

并没有太多的有用信息,直接进入nginx-ingress-controller的pod去测试下网络问题,首先进入nginx-ingress-controller的pod:

代码语言:javascript复制
kubectl exec -n default -it ingress-nginx-controller-57648496fc-dbv26 -- /bin/bash

尝试安装个redis-cli

代码语言:javascript复制
apk add redis
ERROR: Unable to lock database: Permission denied

竟然失败了,我们没有root权限,那么换成root身份进入吧

代码语言:javascript复制
kubectl exec -n default -it ingress-nginx-controller-57648496fc-dbv26 --user=root -- /bin/bash
error: auth info "root" does not exist

竟然没有root身份,咋办呢?kubectl通过pod name无法找到root用户,那么我们直接通过docker 的容器id的root身份进入吧,先获取容器id

代码语言:javascript复制
% docker ps 
CONTAINER ID   IMAGE                    COMMAND                  CREATED          STATUS                PORTS  
f5a55b5d52c5   fa59b6fe51ab             "/usr/bin/dumb-init …"   18 minutes ago   Up 18 minutes                                                                                                k8s_controller_ingress-nginx-controller-57648496fc-dbv26_default_88ff9054-7fdd-4b9d-9804-c0ab453db81b_0

然后以root的身份进入

代码语言:javascript复制
docker exec -u 0 -it f5a55b5d52c5 /bin/bash

尝试安装redis-cli

代码语言:javascript复制
bash-5.1# apk add redis
fetch https://dl-cdn.alpinelinux.org/alpine/v3.13/main/x86_64/APKINDEX.tar.gz
274903771976:error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed:ssl/statem/statem_clnt.c:1913:
ERROR: https://dl-cdn.alpinelinux.org/alpine/v3.13/community: Permission denied
https://github.com/microsoft/vscode-remote-release/issues/5052

依然失败,原因是redis这个image使用的baseimage 有问题,重新下一个没有问题的image?这当然不是我的追求,程序员都是能偷懒就偷懒的。

回想下,我们是不是在本地测试过redis服务的连接,本地不是有redis-cli么?直接cp上去试试吧。

代码语言:javascript复制
 kubectl cp /opt/homebrew/bin/redis-cli ingress-nginx-controller-57648496fc-dbv26:/etc/nginx

进入ingress-nginx-controller,运行失败,原因二进制格式不支持,我去竟然忘了是不同的平台。那就换个跨平台的吧。网上找了个redis官网推荐的shell脚本https://redis.io/clients#bash

代码语言:javascript复制
#!/bin/bash

REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
REDIS_PORT="${REDIS_PORT:-6379}"
REDIS_DB="${REDIS_DB:-0}"
CLIENT_VERSION=0.4
REDIS_ARRAY_RANGE="0,-1"


function redis_read_str() {
        typeset REDIS_STR="$@"
        printf %b "$REDIS_STR" | cut -f2- -d  | tr -d 'r'
}

function redis_read_err() {
        typeset REDIS_ERR="$@"
        printf %s "$REDIS_ERR" | cut -f2- -d-
        exit 1
}

function redis_read_int() {
        typeset -i OUT_INT=$(printf %s "$1" | tr -d : | tr -d 'r')
        printf %b "$OUT_INT"
}

function redis_read_bulk() {
        typeset -i BYTE_COUNT=$1
        typeset -i FILE_DESC=$2
        if [[ $BYTE_COUNT -lt 0 ]]; then
                echo ERROR: Null or incorrect string size returned. >&2
    exec {FILE_DESC}>&-
                exit 1
        fi

        echo $(dd bs=1 count=$BYTE_COUNT status=noxfer <&$FILE_DESC 2>/dev/null)
        dd bs=1 count=2 status=noxfer <&$FILE_DESC 1>/dev/null 2>&1 # we are removing the extra character r
}

function redis_read() {

typeset -i FILE_DESC=$1

if [[ $# -eq  2 ]]; then
  typeset -i PARAM_COUNT=$2
  typeset -i PARAM_CUR=1
fi

while read -r socket_data
do
        typeset first_char
        first_char=$(printf %b "$socket_data" | head -c1)

        case $first_char in
                ' ')
                        redis_read_str "$socket_data"
                        ;;
                '-')
                        redis_read_err "$socket_data"
                        ;;
                ':')
                        redis_read_int "$socket_data"
                        ;;
                '$')
                        bytecount=$(printf %b "$socket_data" | cut -f2 -d$ | tr -d 'r')
                        redis_read_bulk "$bytecount" "$FILE_DESC"
                        ;;
                '*')
                        paramcount=$(printf %b "$socket_data" | cut -f2 -d* | tr -d 'r')
      redis_read "$FILE_DESC" "$paramcount"
                        ;;
        esac

if [[ ! -z $PARAM_COUNT ]]; then
  if [[ $PARAM_CUR -lt $PARAM_COUNT ]]; then
    ((PARAM_CUR =1))
    continue
  else
           break
  fi
else
  break
fi

done<&"$FILE_DESC"

}

function redis_compose_cmd() {
    typeset REDIS_PASS="$1"
    printf %b "*2rn$4rnAUTHrn$${#REDIS_PASS}rn$REDIS_PASSrn"
}

function redis_select_db() {
    typeset REDIS_DB="$1"
    printf %b "*2rn$6rnSELECTrn$${#REDIS_DB}rn$REDIS_DBrn"
}


function redis_get_var() {
  typeset REDIS_VAR="$@"
  printf %b "*2rn$3rnGETrn$${#REDIS_VAR}rn$REDIS_VARrn"
}

function redis_set_var() {
  typeset REDIS_VAR="$1"
  shift
  typeset REDIS_VAR_VAL="$@"
  printf %b "*3rn$3rnSETrn$${#REDIS_VAR}rn$REDIS_VARrn$${#REDIS_VAR_VAL}rn$REDIS_VAR_VALrn"
}

function redis_get_array() {
  typeset REDIS_ARRAY="$1"
  RANGE_LOW=$(echo $2 | cut -f1 -d,)
  RANGE_HIGH=$(echo $2 | cut -f2 -d,)
  printf %b "*4rn$6rnLRANGErn$${#REDIS_ARRAY}rn$REDIS_ARRAYrn$${#RANGE_LOW}rn$RANGE_LOWrn$${#RANGE_HIGH}rn$RANGE_HIGHrn"
}

function redis_set_array() {
  typeset REDIS_ARRAY="$1"
  typeset -a REDIS_ARRAY_VAL=("${!2}")

  printf %b "*2rn$3rnDELrn$${#REDIS_ARRAY}rn$REDIS_ARRAYrn"
  for i in "${REDIS_ARRAY_VAL[@]}"
  do
    printf %b "*3rn$5rnRPUSHrn$${#REDIS_ARRAY}rn$REDIS_ARRAYrn$${#i}rn$irn"
  done
}

while getopts g:s:r:P:H:p:d:ha opt; do
  case $opt in
    p)
      REDIS_PW=${OPTARG}
      ;;
    H)
      REDIS_HOST=${OPTARG}
      ;;
    P)
      REDIS_PORT=${OPTARG}
      ;;
    g)
      REDIS_GET=${OPTARG}
      ;;
    a)
      REDIS_ARRAY=1
      ;;
    r)
      REDIS_ARRAY_RANGE=${OPTARG}
      ;;
    s)
      REDIS_SET=${OPTARG}
      ;;
    d)
      REDIS_DB=${OPTARG}
      ;;
    h)
      echo
      echo USAGE:
      echo "  $0 [-a] [-r <range>] [-s <var>] [-g <var>] [-p <password>] [-d <database_number>] [-H <hostname>] [-P <port>]"
      echo
      exit 1
      ;;
  esac
done

if [[ -z $REDIS_GET ]] && [[ -z $REDIS_SET ]]; then
  echo "You must either GET(-g) or SET(-s)" >&2
  exit 1
fi

exec {FD}<> /dev/tcp/"$REDIS_HOST"/"$REDIS_PORT"

redis_select_db "$REDIS_DB" >&$FD
redis_read $FD 1>/dev/null 2>&1

if [[ ! -z $REDIS_PW ]]; then
  redis_compose_cmd "$REDIS_PW" >&$FD
    redis_read $FD 1>/dev/null 2>&1
fi

if [[ ! -z $REDIS_GET ]]; then
  if [[ $REDIS_ARRAY -eq 1 ]]; then
    redis_get_array "$REDIS_GET" "$REDIS_ARRAY_RANGE" >&$FD
    IFS=$'n'

    for i in $(redis_read $FD)
    do
      echo $i
    done

  else
    redis_get_var "$REDIS_GET" >&$FD
    redis_read $FD
  fi

  exec {FD}>&-
  exit 0
fi

while read -r line
do
        REDIS_TODO=$line
done </dev/stdin

if [[ ! -z $REDIS_SET ]]; then
  if [[ $REDIS_ARRAY -eq 1 ]]; then
    set -- $REDIS_TODO
    typeset -a temparray=( $@ )
    redis_set_array "$REDIS_SET" temparray[@] >&$FD
    redis_read $FD 1>/dev/null 2>&1
  else
    redis_set_var "$REDIS_SET" "$REDIS_TODO" >&$FD
    redis_read $FD 1>/dev/null 2>&1
  fi
  exec {FD}>&-
  exit 0
fi

居然坑爹了,感觉redis官网的大佬写脚本的水平也一般(先嘲讽下)

代码语言:javascript复制
% bash ../../ingress/access_by_lua_block/redis.cli.sh  -H "127.0.0.1" -P 30379 -p 123456 -g "key"
../../ingress/access_by_lua_block/redis.cli.sh: line 170: exec: {FD}: not found

山不到默罕默德这边来,默罕默德就到山那边去

既然跨平台脚本不行,我们就适用平台吧,先看下具体是什么平台

代码语言:javascript复制
# uname -a
Linux ingress-nginx-controller-57648496fc-dbv26 5.10.47-linuxkit #1 SMP PREEMPT Sat Jul 3 21:50:16 UTC 2021 x86_64 Linux

来个go跨平台编译吧

代码语言:javascript复制
package main

import (
  "fmt"

  "github.com/go-redis/redis"
)

func main() {
  fmt.Println("golang连接redis")

  client := redis.NewClient(&redis.Options{
    Addr:     "redis:6379",
    Password: "123456",
    DB:       0,
  })

  pong, err := client.Ping().Result()
  fmt.Println(pong, err)

}
代码语言:javascript复制
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o redis-cli redis-cli.go

cp到ingress-nginx-controller,测试下:

代码语言:javascript复制
 # ./redis-cli 
golang连接redis
PONG <nil>

成功了,查看下我们nginx-lua的代码,一模一样啊

代码语言:javascript复制
local red = require "resty.redis"
local redis = red:new()
redis:set_timeout(1000)
local ok, err = redis:connect("redis", 6379)

既然本地直接执行go 的redis client能够连上,说明网络是通的,排除了网络问题,只能是Ingress配置写的有问题。

查k8s的wiki发现,service的短名称是解析不了的, 需要使用

serviceName.namespace.svc.cluster.local

改下配置吧:

代码语言:javascript复制
local ok, err = redis:connect("redis.default.svc.cluster.local", 6379)
代码语言:javascript复制
% curl -H "token:12344" 127.0.0.1/apple
/apple%

终于成功了

总结下吧,复杂的问题总是有一个简单的内核,如何把大象放进冰箱里?分三步:打开冰箱门,放入大象,关门。我们一步步验证,缩小排查的范围,真相离我们就不远了。

0 人点赞