目录

GO语言开发实践-pod按需连接arthas服务器

pod 按需连接到 arthas tunnel server.

动机

  • arthas 虽然提供了便捷的访问控制台,但是他有2个地方比较麻烦,一个是他没有权限控制,一个是要在 pod 里面去执行,让 agent 连接到 arthas ,这就比较让人头疼了,因为平常肯定不会把进入容器的权限给到开发,我们也不可能每次都帮开发进去里面执行指令,不然他每次重新发版都要给他执行一次就很麻烦,于是我们就需要一个平台来对接 pod 和 arthas。

  • 虽然可以让开发将 agent 集群到代码里面,但是这样就很浪费资源,很多时候根本不需要用的情况,他也一直连着。而且还会在真需要用的时候,出现 arthas 连不上或者黑屏的情况。所以我们干脆自己搞一个平台,然后权限也可控,操作也方便。

效果展示

/go%E8%AF%AD%E8%A8%80%E5%BC%80%E5%8F%91%E5%AE%9E%E8%B7%B5-pod%E6%8C%89%E9%9C%80%E8%BF%9E%E6%8E%A5arthas%E6%9C%8D%E5%8A%A1%E5%99%A8/podToArthas-1.png
pod管理
/go%E8%AF%AD%E8%A8%80%E5%BC%80%E5%8F%91%E5%AE%9E%E8%B7%B5-pod%E6%8C%89%E9%9C%80%E8%BF%9E%E6%8E%A5arthas%E6%9C%8D%E5%8A%A1%E5%99%A8/podToArthas-2.png
arthas管理

实现

实现原理:

  • 先将 k8s 集群信息录入到数据库(后续还要添加集群管理的前端),然后使用 client-go 从数据库读取集群信息并把所有的集群 clientset 记录到内存里面,方便我们可以在任何时候调用。

  • 连接 arthas 的过程利用 client-go 进入到容器里面获取进程 id ,然后再组合命令,再通过 client-go 远程调用容器执行 arthas 的 as.sh 脚本。

  • 需要注意的点就是容器需要 bash 环境,基础镜像要用带 bash 的,不然 arthas 的脚本会无法执行。

  • 执行完成后,将 agent_id 返回给前端,前端弹窗展示,如果不小心把弹窗关了,重新执行一次即可。

  • 然后跳转或者手动选 arthas 环境查看,目前我们环境比较割裂,就只能通过不同的菜单来区分不同的环境,如果都是在同一个网络且用同一套 arthas 就不用那么麻烦了。

核心代码

后端代码片段

 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
func (p *PodService) podExecCommand(req *vo.PodListRequest) (string, string, error) {
    // 使用bytes.Buffer变量接收标准输出和标准错误
    var (
        containerName     string
        getAgentIdCmd     string
        getAgentIdCmdList []string
        arthasServer      string = p.K8sClients.ArthasServersMap[req.Env+req.Name]
    )

    _, ok := p.K8sClients.ClientsMap[req.Env+req.Name]
    if !ok {
        return "", "", errors.New("所选环境或集群不存在")
    }
    // 获取容器名,这个需要规范,我们都是容器名等于服务名,同时也等于工作负载名
    pod, err := p.K8sClients.ClientsMap[req.Env+req.Name].CoreV1().Pods(req.Namespace).Get(context.Background(), req.PodName, metav1.GetOptions{})
    if err != nil {
        return "", "", err
    }
    for _, c := range pod.Spec.Containers {
        r := regexp.MustCompile(`\S+`)
        res := (r.FindAllString(c.Name, -1))
        containerName = res[0]
        break
    }

    var (
        getPidCmd     string = "ps -ef|grep " + containerName + "|grep -v grep|awk '{print $2}'"
        getPidCmdList        = []string{"bash", "-c", getPidCmd}
    )

    // 获取容器内java程序的pid
    pid, _, err := execCommand(p.K8sClients, req, containerName, getPidCmdList)
    if err != nil {
        return "", "", err
    }

    // 连接到arthas
    getAgentIdCmd = "/opt/tools/arthas/as.sh --tunnel-server '" + arthasServer + "' -c 'session' " + strings.TrimSpace(pid) + "|grep AGENT_ID"
    getAgentIdCmdList = []string{"bash", "-c", getAgentIdCmd}
    stdout, stderr, err := execCommand(p.K8sClients, req, containerName, getAgentIdCmdList)
    if err != nil {
        return "", "", err
    }

    // 返回数据
    return stdout, stderr, nil
}

前端代码片段

 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
30
31
32
33
34
35
36
37
  async function onAttachArthas(row) {
    loading.value = true;
    // 深拷贝
    const obj = JSON.parse(JSON.stringify(row));

    // 给proxy对象赋值
    const pod = reactive({
      env: obj.env,
      name: obj.name,
      podName: obj.podName,
      namespace: obj.namespace
    });
    await podAttachArthas(pod)
      .then(res => {
        if (res.success && res.data) {
          agent_id.value = res.data.result;
          ElMessageBox.alert("请复制id:\n" + agent_id.value, "连接成功", {
            customStyle: { "max-width": "35%" },
            // if you want to disable its autofocus
            // autofocus: false,
            confirmButtonText: "OK"
          });
        } else if (res.data) {
          message(res.data.cause, {
            type: "error"
          });
        }
      })
      .catch(err => {
        message("连接失败,请重试 " + err, {
          type: "warning"
        });
      })
      .finally(() => {
        loading.value = false;
      });
  }

难点

前端在做 arthas ui 转发的时候,会碰到 https 跨域的问题,这个比较好解决,arthas UI 前面再加一层代理解决,还有就是 ws 变成 wss ,如果是自建的 nginx ,需要做一下配置。

 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
upstream darthas_server {
    server 192.168.2.217:30358;
}
upstream darthas_server_ws {
    server 192.168.2.217:32102;
}

map $http_upgrade $connection_upgrade {
    default Upgrade;
    ''      close;
}

server {
    listen 443 ssl;
    server_name arthas.yourdomain.com.cn;
    ssl_certificate ssl/yourssl.pem;
    ssl_certificate_key ssl/yourssl.key;

    location / {
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Port $server_port;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://darthas_server;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        # 此项允许执行的 shell 窗口保持开启,最长可达15分钟。不使用此参数的话,默认1分钟后自动关闭。
        proxy_read_timeout 900s;
        proxy_buffering off;
    }
}

server {
    listen 7777 ssl;
    server_name arthas.yourdomain.com.cn;
    ssl_certificate ssl/yourssl.pem;
    ssl_certificate_key ssl/yourssl.key;

    location /ws {
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Port $server_port;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://darthas_server_ws;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        # 此项允许执行的 shell 窗口保持开启,最长可达15分钟。不使用此参数的话,默认1分钟后自动关闭。
        proxy_read_timeout 900s;
        proxy_buffering off;
    }
}

server {
    listen 80;
    server_name arthas.yourdomain.com.cn;
    return 301 https://$server_name$request_uri;
}

最后

代码目前不进行开源,本人技术也有限,就自己玩玩,大家有兴趣的话可以自己尝试一下。