在学习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%
终于成功了
总结下吧,复杂的问题总是有一个简单的内核,如何把大象放进冰箱里?分三步:打开冰箱门,放入大象,关门。我们一步步验证,缩小排查的范围,真相离我们就不远了。