版权声明:本文为博主原创文章,博客地址:
https://blog.csdn.net/zxy_666/article/details/79958948,未经博主允许不得转载。
公司容器云项目需在平台界面上提供一个ping工具,实现从任何pod内ping指定IP.
背景说明:
· 容器云项目
容器云项目是基于kubernetes(简称k8s)集群搭建的应用容器管理平台。集群中的节点是虚拟机或物理机,节点分为master节点和node节点(worker节点)。node节点上运行着pod(k8s集群的最小工作单元),pod中运行着容器,数量随意,但公司的项目里一个pod只运行一个容器。其中,容器是应用提供服务的载体,应用打成镜像后用于创建相应的容器。
每个节点都有各自的IP,每个pod也有他们的IP,通常在集群搭建时,给pod分配指定网段内的IP,由flannel实现集群间网络的通信,flannel保证集群中的每个pod都被分配不同的IP,避免了网络冲突。· ping需求分析
集群外可以ping通集群的节点IP,却ping不通集群内的ip,如pod的ip。但是应用方pod提供的服务会出现访问不了的情况,运维人员需登录到集群上查看响应pod的网络情况。为了解放运维人员的部分劳动力,故有此需求,希望在容器云平台界面直接提供一个ping工具,让使用者可以直接从每个pod实例发出ping命令,ping目标的IP由使用者提供,发出ping请求的源pod的ip就是该pod在集群中创建时被分配的ip,前端可获取得到。
依次尝试了3种方案,直到第三种才实现了需求。
方案:
用go实现ping的原理,参数是ping的目标IP和ping请求次数。限制是:该方案ping的源IP无法更改,默认就是发出ping操作的机器IP。
实现:
注:本程序(ICMP协议)需要root权限才可执行,启动需要sudo权限
package main
import (
"net"
"time"
"fmt"
"strconv"
"os"
)
type PingOption struct{
Count int
Size int
Timeout int64
Nerverstop bool
}
func NewPingOption()*PingOption{
return &PingOption{
Count:4,
Size:32,
Timeout:1000,
Nerverstop:false,
}
}
func main(){
//argsmap:=map[string]interface{}{}
//ping3("www.yeepay.com",argsmap)//10.151.30.227 不存在:67.4.3.2(现在又存在了) 公网IP:63.142.250.4(通)
argsmap:=map[string]interface{}{}
p:=NewPingOption()
p.ping3("www.baidu.com",argsmap)
}
//ping连接用的协议是ICMP,原理:
//Ping的基本原理是发送和接受ICMP请求回显报文。接收方将报文原封不动的返回发送方,发送方校验报文,校验成功则表示ping通。
//一台主机向一个节点发送一个类型字段值为8的ICMP报文,如果途中没有异常(如果没有被路由丢弃,目标不回应ICMP或者传输失败),
//则目标返回类型字段值为0的ICMP报文,说明这台主机可达
func (p *PingOption)ping3(host string, args map[string]interface{}) {
//要发送的回显请求数
var count int = 4
//要发送缓冲区大小,单位:字节
var size int = 32
//等待每次回复的超时时间(毫秒)
var timeout int64 = 1000
//Ping 指定的主机,直到停止
var neverstop bool = false
fmt.Println(args,"args")
if len(args)!=0{
count = args["n"].(int)
size = args["l"].(int)
timeout = args["w"].(int64)
neverstop = args["t"].(bool)
}
//查找规范的dns主机名字 eg.www.baidu.com->www.a.shifen.com
cname, _ := net.LookupCNAME(host)
starttime := time.Now()
//此处的链接conn只是为了获得ip := conn.RemoteAddr(),显示出来,因为后面每次连接都会重新获取conn,todo 但是每次重新获取的conn,其连接的ip保证一致么?
conn, err := net.DialTimeout("ip4:icmp", host, time.Duration(timeout*1000*1000))
//每个域名可能对应多个ip,但实际连接时,请求只会转发到某一个上,故需要获取实际连接的远程ip,才能知道实际ping的机器是哪台
// ip := conn.RemoteAddr()
// fmt.Println("正在 Ping " + cname + " [" + ip.String() + "] 具有 32 字节的数据:")
var seq int16 = 1
id0, id1 := genidentifier3(host)
//ICMP报头的长度至少8字节,如果报文包含数据部分则大于8字节。
//ping命令包含"请求"(Echo Request,报头类型是8)和"应答"(Echo Reply,类型是0)2个部分,由ICMP报头的类型决定
const ECHO_REQUEST_HEAD_LEN = 8
//记录发送次数
sendN := 0
//成功应答次数
recvN := 0
//记录失败请求数
lostN := 0
//所有请求中应答时间最短的一个
shortT := -1
//所有请求中应答时间最长的一个
longT := -1
//所有请求的应答时间和
sumT := 0
for count > 0 || neverstop {
sendN++
//ICMP报文长度,报头8字节,数据部分32字节
var msg []byte = make([]byte, size+ECHO_REQUEST_HEAD_LEN)
//第一个字节表示报文类型,8表示回显请求
msg[0] = 8 // echo
//ping的请求和应答,该code都为0
msg[1] = 0 // code 0
//校验码占2字节
msg[2] = 0 // checksum
msg[3] = 0 // checksum
//ID标识符 占2字节
msg[4], msg[5] = id0, id1 //identifier[0] identifier[1]
//序号占2字节
msg[6], msg[7] = gensequence3(seq) //sequence[0], sequence[1]
length := size + ECHO_REQUEST_HEAD_LEN
//计算检验和。
check := checkSum3(msg[0:length])
//左乘右除,把二进制位向右移动位
msg[2] = byte(check >> 8)
msg[3] = byte(check & 255)
conn, err = net.DialTimeout("ip:icmp", host, time.Duration(timeout*1000*1000))
//todo test
//ip := conn.RemoteAddr()
fmt.Println("remote ip:",host)
checkError3(err)
starttime = time.Now()
//conn.SetReadDeadline可以在未收到数据的指定时间内停止Read等待,并返回错误err,然后判定请求超时
conn.SetDeadline(starttime.Add(time.Duration(timeout * 1000 * 1000)))
//onn.Write方法执行之后也就发送了一条ICMP请求,同时进行计时和计次
_, err = conn.Write(msg[0:length])
//在使用Go语言的net.Dial函数时,发送echo request报文时,不用考虑i前20个字节的ip头;
// 但是在接收到echo response消息时,前20字节是ip头。后面的内容才是icmp的内容,应该与echo request的内容一致
const ECHO_REPLY_HEAD_LEN = 20
var receive []byte = make([]byte, ECHO_REPLY_HEAD_LEN+length)
n, err := conn.Read(receive)
_ = n
var endduration int = int(int64(time.Since(starttime)) / (1000 * 1000))
sumT += endduration
time.Sleep(1000 * 1000 * 1000)
//除了判断err!=nil,还有判断请求和应答的ID标识符,sequence序列码是否一致,以及ICMP是否超时(receive[ECHO_REPLY_HEAD_LEN] == 11,即ICMP报头的类型为11时表示ICMP超时)
if err != nil || receive[ECHO_REPLY_HEAD_LEN+4] != msg[4] || receive[ECHO_REPLY_HEAD_LEN+5] != msg[5] || receive[ECHO_REPLY_HEAD_LEN+6] != msg[6] || receive[ECHO_REPLY_HEAD_LEN+7] != msg[7] || endduration >= int(timeout) || receive[ECHO_REPLY_HEAD_LEN] == 11 {
lostN++
//todo
//fmt.Println("对 " + cname + "[" + ip.String() + "]" + " 的请求超时。")
fmt.Println("对 " + cname + "[" + host + "]" + " 的请求超时。")
} else {
if shortT == -1 {
shortT = endduration
} else if shortT > endduration {
shortT = endduration
}
if longT == -1 {
longT = endduration
} else if longT < endduration {
longT = endduration
}
recvN++
ttl := int(receive[8])
// fmt.Println(ttl)
//todo
//fmt.Println("来自 " + cname + "[" + ip.String() + "]" + " 的回复: 字节=32 时间=" + strconv.Itoa(endduration) + "ms TTL=" + strconv.Itoa(ttl))
fmt.Println("来自 " + cname + "[" + host + "]" + " 的回复: 字节=32 时间=" + strconv.Itoa(endduration) + "ms TTL=" + strconv.Itoa(ttl))
}
seq++
count--
}
//todo 先注释,用下一行测试
//stat3(ip.String(), sendN, lostN, recvN, shortT, longT, sumT)
stat3(host, sendN, lostN, recvN, shortT, longT, sumT)
}
func checkSum3(msg []byte) uint16 {
sum := 0
length := len(msg)
for i := 0; i < length-1; i += 2 {
sum += int(msg[i])*256 + int(msg[i+1])
}
if length%2 == 1 {
sum += int(msg[length-1]) * 256 // notice here, why *256?
}
sum = (sum >> 16) + (sum & 0xffff)
sum += (sum >> 16)
var answer uint16 = uint16(^sum)
return answer
}
func checkError3(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}
func gensequence3(v int16) (byte, byte) {
ret1 := byte(v >> 8)
ret2 := byte(v & 255)
return ret1, ret2
}
func genidentifier3(host string) (byte, byte) {
return host[0], host[1]
}
func stat3(ip string, sendN int, lostN int, recvN int, shortT int, longT int, sumT int) {
fmt.Println()
fmt.Println(ip, " 的 Ping 统计信息:")
fmt.Printf(" 数据包: 已发送 = %d,已接收 = %d,丢失 = %d (%d%% 丢失),\n", sendN, recvN, lostN, int(lostN*100/sendN))
fmt.Println("往返行程的估计时间(以毫秒为单位):")
if recvN != 0 {
fmt.Printf(" 最短 = %dms,最长 = %dms,平均 = %dms\n", shortT, longT, sumT/sendN)
}
}
以上代码摘录自:https://studygolang.com/articles/6733
前情回顾:
因为容器云平台本身也是以pod的形式运行在kubernetes集群中,只不过提供了一个操作平台,其他应用的上线都通过该平台发布,实际上就是该平台拿到应用发布所需的所有信息,调kubernetes的client-go去创建响应的应用pod。而ping工具是由容器云平台提供的,也就是在容器云的项目代码里,如果使用方案一,ping的源IP就是容器云应用的pod的IP,这显然与需求不符。需求是任意pod均可作为发出ping命令的源(容器云平台界面有每个pod实例相关的各种信息,包括podIP)。
鉴于需求的特殊性,衍生了一种思路,即通过go的远程登录工具实现。基本思路是:
· 从容器云应用的pod里登录到发出ping的pod中
· 然后在该pod中发出ping命令
实现:
1. 从yce的pod里登录到master节点(master节点可以对集群中所有资源进行操作,包括pod )
2. 从master节点上登录到发出ping的pod中(登录命令使用的是k8s的集群的操作工具kubectl)
3. 在该pod中发出ping命令
关键代码:
1. session, err := connect(user, pass, srcIP, 22)
2. err=session.Run("pwd; ls; kubectl exec -it "+podName+" -n configcenter bash; ls; ping -c 3 "+dstIP)
package main
import (
"net/http"
//第三方依赖包,需下载
"golang.org/x/crypto/ssh"
"time"
"net"
"fmt"
"os"
)
func main(){
//集群外访问k8s集群pod通过:http://nodeIP:nodePort/xxx访问
//nodeip:18.11.55.44 nodePort:32099
http.HandleFunc("/pingWithSrc",PingWithSrc)
http.ListenAndServe(":8080",nil)
}
func PingWithSrc(w http.ResponseWriter, r *http.Request){
//预设登录信息
user:="master节点的登录密码"
pass:="master节点的登录密码"
//解析参数
if r.Header.Get("user")!=""{
user=r.Header.Get("user")
}
if r.Header.Get("pass")!=""{
pass=r.Header.Get("pass")
}
srcIP:=r.Header.Get("srcIP")
dstIP:=r.Header.Get("dstIP")
count:=r.Header.Get("count")
podName:=r.Header.Get("podName")
fmt.Println("header:",srcIP,dstIP,count,user,pass,podName)
//!!!
//登录到指定机器(这里是master节点)
session, err := connect(user, pass, srcIP, 22)
if err != nil {
fmt.Println(err)
}
defer session.Close()
fmt.Println("enter node:"+srcIP)
//输出重定向(这里把session.Run()的执行结果输出到w中,请求时会打印到浏览器页面上)
session.Stdout = w
session.Stderr = os.Stderr
//!!!
//session.Run在一个进程里只能执行一次,若执行多条将报“Run"已经打开还是运行之类的错误
//若想执行多条命令,命令间用" ; "分号隔开即可
//"kubectl exec -it "+podName+" -n configcenter bash"是k8s的命令,表示登录到指定namespace下的指定pod中。
//接着在该pod中执行ping命令,指定ping时一定要指定请求次数(-c 次数),否则程序会不断发出请求,就无法返回了
err=session.Run("pwd; ls; kubectl exec -it "+podName+" -n configcenter bash; ls; ping -c 3 "+dstIP)
if err!=nil{
fmt.Println("err:",err)
w.Write([]byte("失败:"+srcIP+" ping "+dstIP+" 不通"))
}else{
w.Write([]byte("成功:"+srcIP+" ping "+dstIP+" 通"))
}
}
func connect(user, password, host string, port int) (*ssh.Session, error) {
var (
auth []ssh.AuthMethod
addr string
clientConfig *ssh.ClientConfig
client *ssh.Client
session *ssh.Session
err error
)
// get auth method
auth = make([]ssh.AuthMethod, 0)
auth = append(auth, ssh.Password(password))
clientConfig = &ssh.ClientConfig{
User: user,
Auth: auth,
Timeout: 30 * time.Second,
//需要验证服务端,不做验证返回nil就可以,点击HostKeyCallback看源码就知道了
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
}
// connet to ssh
addr = fmt.Sprintf("%s:%d", host, port)
if client, err = ssh.Dial("tcp", addr, clientConfig); err != nil {
return nil, err
}
// create session
if session, err = client.NewSession(); err != nil {
return nil, err
}
return session, nil
}
以上代码摘录自:
http://www.jb51.net/article/90153.htm
实现:
用go实现模拟的shell交互终端
以下代码摘录自:
https://www.studygolang.com/articles/5004
package main
import (
"net/http"
"golang.org/x/crypto/ssh"
"time"
"net"
"os"
"log"
)
func main(){
PingShell()
}
func PingShell(){
check := func(err error, msg string) {
if err != nil {
log.Fatalf("%s error: %v", msg, err)
}
}
//!!!
client, err := ssh.Dial("tcp", "目标机器的IP:22", &ssh.ClientConfig{
User: "目标机器的登录账号",
Auth: []ssh.AuthMethod{ssh.Password("目标机器的登录密码")},
Timeout: 30 * time.Second,
//需要验证服务端,不做验证返回nil就可以,点击HostKeyCallback看源码就知道了
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
})
check(err, "dial")
session, err := client.NewSession()
check(err, "new session")
defer session.Close()
//输出重定向到标准输出流
session.Stdout = os.Stdout
session.Stderr = os.Stderr
session.Stdin = os.Stdin
modes := ssh.TerminalModes{
ssh.ECHO: 0,
ssh.TTY_OP_ISPEED: 14400,
ssh.TTY_OP_OSPEED: 14400,
}
err = session.RequestPty("xterm", 25, 100, modes)
check(err, "request pty")
err = session.Shell()
check(err, "start shell")
err = session.Wait()
check(err, "return")
}
程序运行效果:
若在本地发出ping命令,即发出ping的ip默认是本机,可以不用实现ping的原理,go提供的工具,一句话就能搞定。
实现:
从本机直接ping指定IP
package main
import (
"net/http"
"os/exec"
)
func main(){
http.HandleFunc("/ping",Ping)
http.ListenAndServe(":8080",nil)
}
func Ping(w http.ResponseWriter, r *http.Request){
dstIP:=r.Header.Get("dstIP")
//!!!
cmd := exec.Command("ping","-c","3", dstIP)
cmd.Stdout = w
err:=cmd.Run()
if err!=nil{
w.Write([]byte(err.Error()))
}
}
结果: 现在需求是能实现了,但该方案存在不安全隐患,因为登录到了集群的节点上。 于是,又衍生了第三种方案。
前情回顾:
方案二被否定后,引出了以下2种思路。
* 参照kubectl exec命令调apiserver
(k8s集群master节点上的组件,集群所有操作都通过api调用它实现)登录进pod的方式,从代码里调apiserver登录到pod内相关的api,实现代码直接登录到pod内,不经过节点。
* 在每个pod内引入故障排查容器。
第一种思路可以实现,但可能稍微费劲点,因为需查看client-go(官方提供的对apiserver api封装的客户端包,通过它访问apiserver,操作k8s集群)相关源码,且过低版本的client-go并为提供该api,设计client-go升级问题。
第二思路非常棒。因为pod内可以有多个容器,且pod内的每个容器共享网络,也就是说pod被分配的IP就是其中每个容器的IP。所以,在每个pod里新增一个故障排除容器(均使用同一个镜像生成),该容器提供一个api,请求提供ping的目标IP(当然加上请求次数也可),然后该容器直接ping目标IP,并返回ping结果。这样一来,在yce的pod里调用欲发出ping的源容器的上述api,yce的pod拿到结果后再返回前端展示。
第2种思路很好,是一种旁路控制的思维,一个pod里有2个容器,一个提供业务逻辑,而另一个可以专注故障排除(无论是网路,系统还是系统组件方面的故障),扩展性很好,不只是ping,以后可以添加其他功能,如telnet等。
实现:
新建一个服务,直接发出ping(发出ping的源IP默认是服务所在的机器IP)
package main
import (
"net/http"
"os/exec"
"github.com/julienschmidt/httprouter"
"github.com/maxwell92/gokits/log"
"time"
"strconv"
"bytes"
"fmt"
)
var logger = log.Log
func main() {
router:=httprouter.New()
router.GET("/ping/:dstIP/:count",Ping)
//一个pod内的容器监听端口不能冲突,由于业务容器监听8080,这就取8090吧
logger.Infoln("troubleshooting listen at port: 8090")
http.ListenAndServe(":8090", router)
}
func Ping(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
fmt.Println("---------enter ping")
dstIP := params.ByName("dstIP")
count := params.ByName("count")
countInt, _ := strconv.ParseInt(count, 10, 64)
if countInt < 1 || countInt > 10 {
w.Write([]byte("请求次数只能在1-10之间"))
return
}
var buf bytes.Buffer
//关键代码
cmd := exec.Command("ping", "-c", count, dstIP)
cmd.Stdout = &buf
go run(cmd, dstIP)
time.Sleep(time.Second * time.Duration(countInt))
if buf.String() != "" {
w.Write([]byte(buf.String()))
} else {
w.Write([]byte("ping " + dstIP + " failed"))
}
}
func run(cmd *exec.Cmd, dstIP string) {
//关键代码
err := cmd.Run()
if err != nil {
logger.Errorf("ping %s failed. err=", dstIP, err)
}
}
故障诊断容器:
将上面的代码打成docker镜像,并使用该镜像创建故障容器。
并在容器云项目代码里添加一个api,供云平台前端调用。形如:
“/api/v2/ping/srcIP/:srcIP/dstIP/:dstIP/count/:count”
package debugContainer
import (
"app/backend/upgrade/controller"
"github.com/julienschmidt/httprouter"
"net/http"
"strconv"
"github.com/maxwell92/gokits/log"
myerror "app/backend/common/yce/error"
"io/ioutil"
"net"
)
var logger = log.Log
type PingController struct {
controller.YceController
}
func (p *PingController) Get(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
//发出ping的容器IP
srcIP := params.ByName("srcIP")
dstIP := params.ByName("dstIP")
//ping的请求次数
count := params.ByName("count")
countInt, _ := strconv.ParseInt(count, 10, 64)
if countInt < 1 || countInt > 10 {
logger.Errorf("PingController failed : count=%s. 请求次数只能在1-10之间")
p.Ye = myerror.NewYceError(myerror.EARGS, "")
p.Ye.Message = p.Ye.Message + ":请求次数count只能在1-10之间"
return
}
//校验IP格式格式正确
if checkIP(srcIP) == nil || checkIP(dstIP) == nil {
logger.Errorf("PingController failed. IP地址格式错误:srcIP=%s,dstIP=%s", srcIP, dstIP)
p.Ye = myerror.NewYceError(myerror.EARGS, "")
p.Ye.Message = p.Ye.Message + ":IP地址格式错误"
return
}
targetURL := "http://" + srcIP + ":8090/ping/" + dstIP + "/" + count
res, err := http.Get(targetURL)
if err != nil {
logger.Errorf("PingController request targeURL failed. err=%s", err)
p.Ye = myerror.NewYceError(myerror.DEBUGCONT_PING_REQ_ERR, "")
return
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
logger.Errorf("PingController read from res.body error. err=%s", err)
p.Ye = myerror.NewYceError(myerror.EGDK_IOUTIL, "")
return
}
//返回前端,需根据自己实际情况修改
p.WriteOk(w, string(body))
}
func checkIP(ip string) []byte {
return net.ParseIP(ip)
}
结果:至此,ping的需求基本落定了。